mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 10:34:53 +00:00
feat: content tab rewrite for worlds (#5136)
* feat: base content card component * fix: tooltips + colors * feat: fix orgs * feat: base content tab internals rewrite * feat: fix invalidmodal * feat: add ContentModpackCard * fix: extract types * draft: layout * feat: unlink modal * feat: impl content tab * fix: lint * fix: toggling * temp: disable updating stuff * feat: selection v-model * feat: bulk selection * feat: mods tab rough draft * feat: use fuse.js * feat: add project combobox * clean up project combobox * feat: start install to play modal * fix: events * feat: use v-on * feat: bulk actions + fix floating action bar width * feat: figma alignments * feat: migrate toggle to tailwind * fix: row borders * feat: disabled state * feat: virtual list impl for card table based on window scroll * fix: lint * feat: virtualization + smaller contentcard items * feat: use ContentCardTable + ContentCardItems * feat: fix gap + border issues on last elm * feat: cleanup + use proper searching * fix: use TeleportOverflowMenu * fix: fallback to svg if src is invalid on avatar component * fix: storybook * feat: start on updater modal * feat: finish content updater modal * feat: i18n pass * feat: impl modal * feat(app): backend changes for content tab refactor (#5237) * feat: include_changelog=false for updater modal * fix: hash overrides * feat: update checking for modpack * feat: qa * feat: modpack content modal * fix: padding in table to match modals + tightness * fix: lint * feat: delete modal * feat: fix toggle bugs * fix: prepr * fix: duplicate messages * qa: full width search * qa: use bg-surface-1.5 * qa: animation for filter pills * qa: standardize hover colors * fix: border-[1px] is border * qa: mass de-select actually mass selecting * qa: match figma designs for floating action bar * qa: modal fixes * q: modal fixes x2 * fix: table border * qa: confirm modals * qa: modal alignment * qa: re-add stuck heading + dedupe logic * qa: dedupe virtual scrolling + remove dead components * qa: responsiveness for content table + link fixes * qa: version column link, tooltips + lint fixes * qa: instance busy protections * fix: installation freeze bug * chore: remove old mods page * refactor: deduplicate layout * chore: delete old content page(s) * qa * qa * qa * feat: sort btn - to iterate * fix: ml * feat: date added * fix: lint * fix: formatting.ts removal * feat: get_dependencies_as_content_items * qa: final QA changes * refactor: deduplicate + polish content.rs * feat: hook up content.vue with v1 * feat: hide v1 content api behind frontend feature flag * fix: query keys + copy on empty state * chore: i18n pass * feat: reimpl unlink + upload endpoint * feat: use bulk endpoints v1 * fix: lint * fix: flags * fix: responsiveness via container queries * fix: lint * qa: 1 * qa: fixes * qa: fix ssr issues with browse content * qa: header page divider * qa: modals * fix: prepr * fix: issues * fix: lint * fix: toggle v1 ff * qa: 5 * qa: delete modal copy * feat: creation flow modals (#5383) * refactor: delete content v0 usages + impl * feat: qa + fixes * feat: installing banner using state event * feat: fix modpack card bugs + filtering issues * refactor: delete backups v0 api module * feat: v1 servers GET endpoint * fix: backups * feat: swap to kyros upload v1 addon * fix: use tanstack for loader.vue * feat: finish install from discovery modal * qa: bug fixes * feat: set up installation settings * fix: lint * fix: typos * fix: bugs * fix: disable inline content * feat: content tab improvements — upload UX, installation settings, and client-only indicators Upload cancellation and navigation guard: - Add ConfirmLeaveModal that prompts when navigating away during upload - Cancel in-flight XHR uploads when user confirms leaving the page - Add beforeunload handler to warn on browser/tab close during upload - Track uploadedBytes/totalBytes in UploadState for progress display - Replace Collapsible with Transition for upload progress admonition - Show byte progress and percentage in upload banner - Clamp upload progress to prevent exceeding 100% Installation settings (server.properties): - Add KnownPropertiesFields and PropertiesFields types to Archon types - Add buildProperties() to creation flow context to collect gamemode, difficulty, seed, world type, structures, and generator settings - Pass properties through installContent on onboarding, discovery, and ServerSetupModal flows Server setup and discovery flow improvements: - Migrate ServerSetupModal from servers_v0.reinstall to content_v1.installContent - Replace loaderApiNames lookup with toApiLoader() helper - Remove eraseDataOnInstall toggle — always use soft_override: false - Simplify modpack install on discovery page to use first available version and route through creation flow modal for both onboarding and non-onboarding - Differentiate post-install navigation: content page for onboarding, loader options for existing servers Modpack update flow: - Replace updateModpack() call with installContent() using soft_override: true to support version selection in the content updater modal Client-only mod indicators: - Add environment field to AddonVersion (reuses Labrinth.Projects.v3.Environment) - Add environment to ContentItem and isClientOnly to ContentCardTableItem - Show orange TriangleAlertIcon with tooltip on client-only mods in content table - Add "Client-only" filter pill to content filtering (controlled via showClientOnlyFilter on ContentManagerContext) - Apply client-only indicators in both ContentPageLayout and ModpackContentModal Misc: - Add CLAUDE.md note about using prepr commands for lint checks - Export ConfirmLeaveModal from instances barrel * fix: piping * fix: switch content disable for linked server instances * feat: client only filter * fix: prepr * feat: hasUpdate shape update * feat: bulk update endpoint impl for content in panel * feat: websocket state impl again with new phases * fix: ws * fix: use timeout fn for sync admon + fix content card layout scroll for browsers with overflow anchor bug * fix: qa bugs * fix: lint, a11y and i18n * refactor: set up layouts folder properly * fix: linked data cache stuff + lint * feat: move installationsettings to shared layout * fix: lint * fix: issues * feat: temp fuck staging up * fix: lockfile * fix: data sync issues on loader.vue * fix: lint * Hide shader configuration files from content list (#5499) * feat: workaround search problem + split out reset * fix: qa * fix: changelog not showing on first open * fix: qa + optimistic updating improvements * fix: prepr+lint * fix: qa * feat: qa * fix: lint * fix: lint * fix: build * fix: build * fix: type errors * fix: fade and JAVA_HOME passthrough * feat: qa * feat: impl diff shit * fix: qa * fix: app qa * feat: update diff modal * fix: endpoint * fix: qa * fix: qa * fix: use bulk in modpack modal * feat: abort signal impl + fix issues * fix: diff modal trunc * feat: qa * fix: qa * feat: tooltip content tab * fix: prepr * fix: dismiss on settings btn * feat: qa * feat: dont clear handlers on disconnect * fix: lint * fix: wrangler + introduce staging-archon env file --------- Signed-off-by: Calum H. <calum@modrinth.com> Co-authored-by: tdgao <mr.trumgao@gmail.com> Co-authored-by: Artyom Ezri <61311568+Artezon@users.noreply.github.com>
This commit is contained in:
co-authored by
tdgao
Artyom Ezri
parent
f0224dfff7
commit
7d92e4ec7f
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
BookmarkIcon,
|
||||
CheckIcon,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
InfoIcon,
|
||||
LeftArrowIcon,
|
||||
ListIcon,
|
||||
MinecraftServerIcon,
|
||||
MoreVerticalIcon,
|
||||
SearchIcon,
|
||||
XIcon,
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
type CreationFlowContextValue,
|
||||
CreationFlowModal,
|
||||
defineMessages,
|
||||
DropdownSelect,
|
||||
injectModrinthClient,
|
||||
@@ -31,30 +34,31 @@ import {
|
||||
SearchSidebarFilter,
|
||||
type SortType,
|
||||
StyledInput,
|
||||
Toggle,
|
||||
useDebugLogger,
|
||||
useSearch,
|
||||
useServerSearch,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { capitalizeString, cycleValue, type Mod as InstallableMod } from '@modrinth/utils'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { capitalizeString, cycleValue } from '@modrinth/utils'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useThrottleFn, useTimeoutFn } from '@vueuse/core'
|
||||
import { computed, type Reactive, watch } from 'vue'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import LogoAnimated from '~/components/brand/LogoAnimated.vue'
|
||||
import AdPlaceholder from '~/components/ui/AdPlaceholder.vue'
|
||||
import { projectQueryOptions } from '~/composables/queries/project'
|
||||
import { versionQueryOptions } from '~/composables/queries/version'
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
import { useModrinthServers } from '~/composables/servers/modrinth-servers.ts'
|
||||
import type { DisplayLocation, DisplayMode } from '~/plugins/cosmetics.ts'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const debug = useDebugLogger('Discover')
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const filtersMenuOpen = ref(false)
|
||||
|
||||
const route = useNativeRoute()
|
||||
const router = useNativeRouter()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const cosmetics = useCosmetics()
|
||||
const tags = useGeneratedState()
|
||||
@@ -62,8 +66,6 @@ const flags = useFeatureFlags()
|
||||
const auth = await useAuth()
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const modrinthClient = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
let prefetchTimeout: ReturnType<typeof useTimeoutFn> | null = null
|
||||
const HOVER_DURATION_TO_PREFETCH_MS = 500
|
||||
@@ -71,14 +73,20 @@ const HOVER_DURATION_TO_PREFETCH_MS = 500
|
||||
const handleProjectMouseEnter = (result: Labrinth.Search.v2.ResultSearchProject) => {
|
||||
const slug = result.slug || result.project_id
|
||||
prefetchTimeout = useTimeoutFn(
|
||||
() => queryClient.prefetchQuery(projectQueryOptions.v2(slug, modrinthClient)),
|
||||
() => {
|
||||
queryClient.prefetchQuery(projectQueryOptions.v2(slug, client))
|
||||
queryClient.prefetchQuery(projectQueryOptions.v3(result.project_id, client))
|
||||
queryClient.prefetchQuery(projectQueryOptions.members(result.project_id, client))
|
||||
queryClient.prefetchQuery(projectQueryOptions.dependencies(result.project_id, client))
|
||||
queryClient.prefetchQuery(projectQueryOptions.versionsV3(result.project_id, client))
|
||||
},
|
||||
HOVER_DURATION_TO_PREFETCH_MS,
|
||||
{ immediate: false },
|
||||
)
|
||||
prefetchTimeout.start()
|
||||
}
|
||||
|
||||
const handleServerProjectMouseEnter = (result: Labrinth.Search.v3.ResultSearchProject) => {
|
||||
const _handleServerProjectMouseEnter = (result: Labrinth.Search.v3.ResultSearchProject) => {
|
||||
const slug = result.slug || result.project_id
|
||||
|
||||
prefetchTimeout = useTimeoutFn(
|
||||
@@ -105,10 +113,6 @@ const currentType = computed(() =>
|
||||
queryAsStringOrEmpty(route.params.type).replaceAll(/^\/|s\/?$/g, ''),
|
||||
)
|
||||
|
||||
watch(currentType, (newType) => {
|
||||
console.log('currentType changed:', newType)
|
||||
})
|
||||
|
||||
const projectType = computed(() => tags.value.projectTypes.find((x) => x.id === currentType.value))
|
||||
const projectTypes = computed(() => (projectType.value ? [projectType.value.id] : []))
|
||||
|
||||
@@ -121,58 +125,104 @@ const resultsDisplayMode = computed<DisplayMode>(() =>
|
||||
: 'list',
|
||||
)
|
||||
|
||||
const server = ref<Reactive<ModrinthServer>>()
|
||||
const serverHideInstalled = ref(false)
|
||||
const eraseDataOnInstall = ref(false)
|
||||
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 PERSISTENT_QUERY_PARAMS = ['sid', 'shi']
|
||||
|
||||
async function updateServerContext() {
|
||||
const serverId = queryAsString(route.query.sid)
|
||||
|
||||
if (!serverId) {
|
||||
server.value = undefined
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (!auth.value.user) {
|
||||
router.push('/auth/sign-in?redirect=' + encodeURIComponent(route.fullPath))
|
||||
return
|
||||
}
|
||||
|
||||
if (!server.value || server.value.serverId !== serverId) {
|
||||
server.value = await useModrinthServers(serverId, ['general', 'content'])
|
||||
}
|
||||
|
||||
if (route.query.shi && projectType.value?.id !== 'modpack' && server.value) {
|
||||
serverHideInstalled.value = route.query.shi === 'true'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load server context:', error)
|
||||
server.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.client && route.query.sid) {
|
||||
updateServerContext().catch((error) => {
|
||||
console.error('Failed to initialize server context:', error)
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.query.sid,
|
||||
() => {
|
||||
updateServerContext().catch((error) => {
|
||||
console.error('Failed to update server context:', error)
|
||||
})
|
||||
const {
|
||||
data: serverData,
|
||||
isLoading: serverDataLoading,
|
||||
error: serverDataError,
|
||||
} = useQuery({
|
||||
queryKey: computed(() => ['servers', 'detail', currentServerId.value] as const),
|
||||
queryFn: () => {
|
||||
debug('serverData queryFn firing for:', currentServerId.value)
|
||||
return client.archon.servers_v0.get(currentServerId.value!)
|
||||
},
|
||||
enabled: computed(() => {
|
||||
const enabled = !!currentServerId.value
|
||||
debug('serverData enabled:', enabled)
|
||||
return enabled
|
||||
}),
|
||||
})
|
||||
|
||||
watch(serverData, (val) =>
|
||||
debug('serverData changed:', val?.server_id, val?.name, val?.loader, val?.mc_version),
|
||||
)
|
||||
watch(serverDataLoading, (val) => debug('serverData loading:', val))
|
||||
watch(serverDataError, (val) => {
|
||||
if (val) debug('serverData error:', val)
|
||||
})
|
||||
|
||||
const serverIcon = computed(() => {
|
||||
if (!currentServerId.value || !import.meta.client) return null
|
||||
return localStorage.getItem(`server-icon-${currentServerId.value}`)
|
||||
})
|
||||
|
||||
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,
|
||||
queryFn: () => client.archon.content_v1.getAddons(currentServerId.value!, currentWorldId.value!),
|
||||
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)
|
||||
handleError(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,
|
||||
projectId,
|
||||
versionId,
|
||||
}: {
|
||||
serverId: string
|
||||
projectId: string
|
||||
versionId: string
|
||||
}) =>
|
||||
client.archon.content_v1.addAddon(serverId, currentWorldId.value!, {
|
||||
project_id: projectId,
|
||||
version_id: versionId,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
if (currentServerId.value) {
|
||||
queryClient.refetchQueries({ queryKey: ['content', 'list', currentServerId.value] })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const PERSISTENT_QUERY_PARAMS = ['sid', 'wid', 'shi', 'from']
|
||||
|
||||
if (route.query.shi && projectType.value?.id !== 'modpack') {
|
||||
serverHideInstalled.value = route.query.shi === 'true'
|
||||
}
|
||||
|
||||
const serverFilters = computed(() => {
|
||||
debug(
|
||||
'serverFilters recomputing, serverData:',
|
||||
!!serverData.value,
|
||||
'projectType:',
|
||||
projectType.value?.id,
|
||||
)
|
||||
const filters = []
|
||||
if (server.value && projectType.value?.id !== 'modpack') {
|
||||
const gameVersion = server.value.general?.mc_version
|
||||
if (serverData.value && projectType.value?.id !== 'modpack') {
|
||||
const gameVersion = serverData.value.mc_version
|
||||
if (gameVersion) {
|
||||
filters.push({
|
||||
type: 'game_version',
|
||||
@@ -180,7 +230,7 @@ const serverFilters = computed(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const platform = server.value.general?.loader?.toLowerCase()
|
||||
const platform = serverData.value.loader?.toLowerCase()
|
||||
|
||||
const modLoaders = ['fabric', 'forge', 'quilt', 'neoforge']
|
||||
|
||||
@@ -200,13 +250,20 @@ const serverFilters = computed(() => {
|
||||
})
|
||||
}
|
||||
|
||||
if (serverHideInstalled.value) {
|
||||
const installedMods = server.value.content?.data
|
||||
.filter((x: InstallableMod) => x.project_id)
|
||||
.map((x: InstallableMod) => x.project_id)
|
||||
.filter((id): id is string => id !== undefined)
|
||||
if (projectType.value?.id === 'mod') {
|
||||
filters.push({
|
||||
type: 'environment',
|
||||
option: 'server',
|
||||
})
|
||||
}
|
||||
|
||||
installedMods
|
||||
if (serverHideInstalled.value && serverContentData.value) {
|
||||
const installedIds = (serverContentData.value.addons ?? [])
|
||||
.filter((x) => x.project_id)
|
||||
.map((x) => x.project_id)
|
||||
.filter((id): id is string => id !== null)
|
||||
|
||||
installedIds
|
||||
.map((x: string) => ({
|
||||
type: 'project_id',
|
||||
option: `project_id:${x}`,
|
||||
@@ -215,6 +272,20 @@ const serverFilters = computed(() => {
|
||||
.forEach((x) => filters.push(x))
|
||||
}
|
||||
}
|
||||
|
||||
if (currentServerId.value && projectType.value?.id === 'modpack') {
|
||||
filters.push(
|
||||
{
|
||||
type: 'environment',
|
||||
option: 'client',
|
||||
},
|
||||
{
|
||||
type: 'environment',
|
||||
option: 'server',
|
||||
},
|
||||
)
|
||||
}
|
||||
debug('serverFilters result:', filters)
|
||||
return filters
|
||||
})
|
||||
|
||||
@@ -256,6 +327,7 @@ const {
|
||||
// Functions
|
||||
createPageParams,
|
||||
} = useSearch(projectTypes, tags, serverFilters)
|
||||
debug('useSearch initialized, requestParams:', requestParams.value)
|
||||
|
||||
const selectedFilterTags = computed(() =>
|
||||
currentFilters.value
|
||||
@@ -315,42 +387,67 @@ interface InstallableSearchResult extends Labrinth.Search.v2.ResultSearchProject
|
||||
}
|
||||
|
||||
async function serverInstall(project: InstallableSearchResult) {
|
||||
if (!server.value) {
|
||||
if (!serverData.value || !currentServerId.value) {
|
||||
handleError(new Error('No server to install to.'))
|
||||
return
|
||||
}
|
||||
project.installing = true
|
||||
try {
|
||||
const versions = (await useBaseFetch(
|
||||
`project/${project.project_id}/version`,
|
||||
{},
|
||||
true,
|
||||
)) as Labrinth.Versions.v2.Version[]
|
||||
|
||||
const version =
|
||||
versions.find(
|
||||
(x) =>
|
||||
x.game_versions.includes(server.value!.general.mc_version) &&
|
||||
x.loaders.includes(server.value!.general.loader.toLowerCase()),
|
||||
) ?? versions[0]
|
||||
|
||||
if (projectType.value?.id === 'modpack') {
|
||||
await server.value.general.reinstall(
|
||||
false,
|
||||
project.project_id,
|
||||
version.id,
|
||||
undefined,
|
||||
eraseDataOnInstall.value,
|
||||
)
|
||||
project.installed = true
|
||||
navigateTo(`/hosting/manage/${server.value.serverId}/options/loader`)
|
||||
} else if (projectType.value?.id === 'mod') {
|
||||
await server.value.content.install('mod', version.project_id, version.id)
|
||||
await server.value.refresh(['content'])
|
||||
project.installed = true
|
||||
} else if (projectType.value?.id === 'plugin') {
|
||||
await server.value.content.install('plugin', version.project_id, version.id)
|
||||
await server.value.refresh(['content'])
|
||||
// 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,
|
||||
})
|
||||
const versionId = versions[0]?.id ?? project.latest_version
|
||||
if (!versionId) {
|
||||
handleError(new Error('No version found for this modpack'))
|
||||
project.installing = false
|
||||
return
|
||||
}
|
||||
const modalInstance = onboardingModalRef.value
|
||||
if (modalInstance) {
|
||||
onboardingInstallingProject.value = project
|
||||
modalInstance.show()
|
||||
await nextTick()
|
||||
const ctx = modalInstance.ctx
|
||||
ctx.setupType.value = 'modpack'
|
||||
ctx.modpackSelection.value = {
|
||||
projectId: project.project_id,
|
||||
versionId,
|
||||
name: project.title,
|
||||
iconUrl: project.icon_url ?? undefined,
|
||||
}
|
||||
ctx.modal.value?.setStage('final-config')
|
||||
}
|
||||
return
|
||||
} else if (
|
||||
projectType.value?.id === 'mod' ||
|
||||
projectType.value?.id === 'plugin' ||
|
||||
projectType.value?.id === 'datapack'
|
||||
) {
|
||||
const versions = await client.labrinth.versions_v2.getProjectVersions(project.project_id)
|
||||
const isDatapack = projectType.value?.id === 'datapack'
|
||||
const version = versions.find((x) => {
|
||||
if (!x.game_versions.includes(serverData.value!.mc_version!)) return false
|
||||
if (isDatapack) return true
|
||||
return x.loaders.includes(serverData.value!.loader!.toLowerCase())
|
||||
})
|
||||
if (!version) {
|
||||
handleError(
|
||||
new Error(
|
||||
isDatapack
|
||||
? `No compatible version found for ${serverData.value!.mc_version}`
|
||||
: `No compatible version found for ${serverData.value!.mc_version} / ${serverData.value!.loader}`,
|
||||
),
|
||||
)
|
||||
project.installing = false
|
||||
return
|
||||
}
|
||||
await installContentMutation.mutateAsync({
|
||||
serverId: currentServerId.value,
|
||||
projectId: version.project_id,
|
||||
versionId: version.id,
|
||||
})
|
||||
project.installed = true
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -361,28 +458,6 @@ async function serverInstall(project: InstallableSearchResult) {
|
||||
}
|
||||
|
||||
const noLoad = ref(false)
|
||||
|
||||
const {
|
||||
serverCurrentSortType,
|
||||
serverCurrentFilters,
|
||||
serverToggledGroups,
|
||||
serverSortTypes,
|
||||
serverFilterTypes,
|
||||
serverRequestParams,
|
||||
createServerPageParams,
|
||||
} = useServerSearch({ tags, query, maxResults, currentPage })
|
||||
|
||||
const effectiveSortType = computed({
|
||||
get: () => (currentType.value === 'server' ? serverCurrentSortType.value : currentSortType.value),
|
||||
set: (v: SortType) => {
|
||||
if (currentType.value === 'server') serverCurrentSortType.value = v
|
||||
else currentSortType.value = v
|
||||
},
|
||||
})
|
||||
const effectiveSortTypes = computed(() =>
|
||||
currentType.value === 'server' ? serverSortTypes : [...sortTypes],
|
||||
)
|
||||
|
||||
const {
|
||||
data: rawResults,
|
||||
refresh: refreshSearch,
|
||||
@@ -390,35 +465,26 @@ const {
|
||||
} = useLazyFetch(
|
||||
() => {
|
||||
const config = useRuntimeConfig()
|
||||
let base = import.meta.server ? config.apiBaseUrl : config.public.apiBaseUrl
|
||||
const 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${serverRequestParams.value}`
|
||||
}
|
||||
|
||||
return `${base}search${requestParams.value}`
|
||||
const url = `${base}search${requestParams.value}`
|
||||
debug('useLazyFetch URL:', url)
|
||||
return url
|
||||
},
|
||||
{
|
||||
watch: false,
|
||||
transform: (
|
||||
hits: Labrinth.Search.v2.SearchResults | Labrinth.Search.v3.SearchResults,
|
||||
): Labrinth.Search.v2.SearchResults => {
|
||||
transform: (hits) => {
|
||||
debug('useLazyFetch transform, hits:', (hits as any)?.total_hits)
|
||||
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
|
||||
return hits as Labrinth.Search.v2.SearchResults
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const results = shallowRef(toRaw(rawResults))
|
||||
watch(searchLoading, (val) => debug('searchLoading:', val))
|
||||
watch(rawResults, (val) => debug('rawResults changed, total_hits:', val?.total_hits))
|
||||
|
||||
const results = computed(() => rawResults.value)
|
||||
const pageCount = computed(() =>
|
||||
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1,
|
||||
)
|
||||
@@ -428,6 +494,14 @@ function scrollToTop(behavior: ScrollBehavior = 'smooth') {
|
||||
}
|
||||
|
||||
function updateSearchResults(pageNumber: number = 1, resetScroll = true) {
|
||||
debug(
|
||||
'updateSearchResults called, page:',
|
||||
pageNumber,
|
||||
'query:',
|
||||
query.value,
|
||||
'requestParams:',
|
||||
requestParams.value,
|
||||
)
|
||||
currentPage.value = pageNumber
|
||||
if (resetScroll) {
|
||||
scrollToTop()
|
||||
@@ -435,9 +509,11 @@ function updateSearchResults(pageNumber: number = 1, resetScroll = true) {
|
||||
noLoad.value = true
|
||||
|
||||
if (query.value === null) {
|
||||
debug('updateSearchResults: query is null, returning early')
|
||||
return
|
||||
}
|
||||
|
||||
debug('updateSearchResults: calling refreshSearch')
|
||||
refreshSearch()
|
||||
|
||||
if (import.meta.client) {
|
||||
@@ -457,7 +533,7 @@ function updateSearchResults(pageNumber: number = 1, resetScroll = true) {
|
||||
|
||||
const params = {
|
||||
...persistentParams,
|
||||
...(currentType.value === 'server' ? createServerPageParams() : createPageParams()),
|
||||
...createPageParams(),
|
||||
}
|
||||
|
||||
router.replace({ path: route.path, query: params })
|
||||
@@ -468,12 +544,6 @@ watch([currentFilters], () => {
|
||||
updateSearchResults(1, false)
|
||||
})
|
||||
|
||||
watch([serverCurrentFilters, serverCurrentSortType], () => {
|
||||
if (currentType.value === 'server') {
|
||||
updateSearchResults(1, false)
|
||||
}
|
||||
})
|
||||
|
||||
const throttledSearch = useThrottleFn(() => updateSearchResults(), 500, true)
|
||||
|
||||
function cycleSearchDisplayMode() {
|
||||
@@ -507,79 +577,116 @@ 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.`,
|
||||
)
|
||||
|
||||
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}/options/loader`
|
||||
return `/hosting/manage/${id}/content`
|
||||
})
|
||||
|
||||
// Onboarding modpack flow: show creation flow modal overlay on discovery page
|
||||
const onboardingModalRef = ref<InstanceType<typeof CreationFlowModal> | null>(null)
|
||||
const onboardingInstallingProject = ref<InstallableSearchResult | null>(null)
|
||||
|
||||
function onOnboardingHide() {
|
||||
if (onboardingInstallingProject.value) {
|
||||
onboardingInstallingProject.value.installing = false
|
||||
onboardingInstallingProject.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function onOnboardingBack() {
|
||||
onboardingModalRef.value?.hide()
|
||||
}
|
||||
|
||||
async function onModpackFlowCreate(config: CreationFlowContextValue) {
|
||||
if (!currentServerId.value || !config.modpackSelection.value) return
|
||||
|
||||
try {
|
||||
await client.archon.content_v1.installContent(currentServerId.value, currentWorldId.value!, {
|
||||
content_variant: 'modpack',
|
||||
spec: {
|
||||
platform: 'modrinth',
|
||||
project_id: config.modpackSelection.value.projectId,
|
||||
version_id: config.modpackSelection.value.versionId,
|
||||
},
|
||||
soft_override: false,
|
||||
properties: config.buildProperties(),
|
||||
} satisfies Archon.Content.v1.InstallWorldContent)
|
||||
|
||||
if (fromContext.value === 'onboarding') {
|
||||
await client.archon.servers_v1.endIntro(currentServerId.value)
|
||||
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', currentServerId.value] })
|
||||
navigateTo(`/hosting/manage/${currentServerId.value}/content`)
|
||||
} else {
|
||||
navigateTo(`/hosting/manage/${currentServerId.value}/options/loader`)
|
||||
}
|
||||
} catch (e) {
|
||||
handleError(new Error(`Error installing modpack: ${e}`))
|
||||
config.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
useSeoMeta({
|
||||
description,
|
||||
ogTitle,
|
||||
ogDescription: description,
|
||||
})
|
||||
|
||||
const serverHits = computed(
|
||||
() =>
|
||||
((rawResults.value as unknown as Labrinth.Search.v3.SearchResults)
|
||||
?.hits as Labrinth.Search.v3.ResultSearchProject[]) ?? [],
|
||||
)
|
||||
|
||||
const getServerModpackContent = (hit: Labrinth.Search.v3.ResultSearchProject) => {
|
||||
const content = hit.minecraft_java_server?.content
|
||||
if (content?.kind === 'modpack') {
|
||||
const { project_name, project_icon, project_id } = content
|
||||
if (!project_name) return undefined
|
||||
return {
|
||||
name: project_name,
|
||||
icon: project_icon,
|
||||
onclick:
|
||||
project_id !== hit.project_id
|
||||
? () => {
|
||||
navigateTo(`/project/${project_id}`)
|
||||
}
|
||||
: undefined,
|
||||
showCustomModpackTooltip: project_id === hit.project_id,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Teleport v-if="flags.searchBackground" to="#absolute-background-teleport">
|
||||
<div class="search-background"></div>
|
||||
</Teleport>
|
||||
<Teleport v-if="server" to="#discover-header-prefix">
|
||||
<Teleport v-if="serverData" to="#discover-header-prefix" defer>
|
||||
<div
|
||||
class="mb-4 flex flex-wrap items-center justify-between gap-3 border-0 border-b border-solid border-divider pb-4"
|
||||
>
|
||||
<nuxt-link
|
||||
:to="`/servers/manage/${server.serverId}/content`"
|
||||
<button
|
||||
tabindex="-1"
|
||||
class="flex flex-col gap-4 text-primary"
|
||||
class="flex cursor-pointer flex-col gap-4 bg-transparent text-primary"
|
||||
@click="navigateTo(serverBackUrl)"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<Avatar
|
||||
:src="
|
||||
server.general.is_medal
|
||||
serverData.is_medal
|
||||
? 'https://cdn-raw.modrinth.com/medal_icon.webp'
|
||||
: server.general.image
|
||||
: (serverIcon ?? MinecraftServerIcon)
|
||||
"
|
||||
size="48px"
|
||||
/>
|
||||
<span class="flex flex-col gap-2">
|
||||
<span class="bold font-extrabold text-contrast">
|
||||
{{ server.general.name }}
|
||||
{{ serverData.name }}
|
||||
</span>
|
||||
<span class="flex items-center gap-2 font-semibold text-secondary">
|
||||
<GameIcon class="h-5 w-5 text-secondary" />
|
||||
{{ server.general.loader }} {{ server.general.mc_version }}
|
||||
{{ serverData.loader }} {{ serverData.mc_version }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</nuxt-link>
|
||||
</button>
|
||||
<ButtonStyled>
|
||||
<nuxt-link :to="`/hosting/manage/${server.serverId}/content`">
|
||||
<button @click="navigateTo(serverBackUrl)">
|
||||
<LeftArrowIcon />
|
||||
Back to server
|
||||
</nuxt-link>
|
||||
{{
|
||||
fromContext === 'onboarding'
|
||||
? 'Back to setup'
|
||||
: fromContext === 'reset-server'
|
||||
? 'Cancel reset'
|
||||
: 'Back to server'
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<h1 class="m-0 text-xl font-extrabold leading-none text-contrast">Install content to server</h1>
|
||||
<h1 class="m-0 text-xl font-extrabold leading-none text-contrast">
|
||||
{{
|
||||
fromContext === 'reset-server'
|
||||
? 'Select modpack to install after reset'
|
||||
: 'Install content to server'
|
||||
}}
|
||||
</h1>
|
||||
</Teleport>
|
||||
|
||||
<aside
|
||||
@@ -588,7 +695,7 @@ const getServerModpackContent = (hit: Labrinth.Search.v3.ResultSearchProject) =>
|
||||
}"
|
||||
aria-label="Filters"
|
||||
>
|
||||
<AdPlaceholder v-if="!auth.user && !server" />
|
||||
<AdPlaceholder v-if="!auth.user && !serverData" />
|
||||
<div v-if="filtersMenuOpen" class="fixed inset-0 z-40 bg-bg"></div>
|
||||
<div
|
||||
class="flex flex-col gap-3"
|
||||
@@ -615,23 +722,7 @@ const getServerModpackContent = (hit: Labrinth.Search.v3.ResultSearchProject) =>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div
|
||||
v-if="server && projectType?.id === 'modpack'"
|
||||
class="card-shadow rounded-2xl bg-bg-raised"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2 px-6 py-4 text-contrast">
|
||||
<h3 class="m-0 text-lg">Options</h3>
|
||||
</div>
|
||||
<div class="flex flex-row items-center justify-between gap-2 px-6">
|
||||
<label for="erase-data-on-install"> Erase all data on install </label>
|
||||
<Toggle id="erase-data-on-install" v-model="eraseDataOnInstall" class="flex-none" />
|
||||
</div>
|
||||
<div class="px-6 py-4 text-sm">
|
||||
If enabled, existing mods, worlds, and configurations, will be deleted before installing
|
||||
the selected modpack.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="server && projectType?.id !== 'modpack'"
|
||||
v-if="serverData && projectType?.id !== 'modpack'"
|
||||
class="card-shadow rounded-2xl bg-bg-raised p-4"
|
||||
>
|
||||
<Checkbox
|
||||
@@ -641,73 +732,41 @@ const getServerModpackContent = (hit: Labrinth.Search.v3.ResultSearchProject) =>
|
||||
@update:model-value="updateSearchResults()"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="currentType === 'server'">
|
||||
<SearchSidebarFilter
|
||||
v-for="filterType in serverFilterTypes.filter((f) => f.options.length > 0)"
|
||||
:key="`server-filter-${filterType.id}`"
|
||||
v-model:selected-filters="serverCurrentFilters"
|
||||
v-model:toggled-groups="serverToggledGroups"
|
||||
:provided-filters="serverFilters"
|
||||
:filter-type="filterType"
|
||||
:class="
|
||||
filtersMenuOpen
|
||||
? 'border-0 border-b-[1px] border-solid border-divider last:border-b-0'
|
||||
: 'card-shadow rounded-2xl bg-bg-raised'
|
||||
"
|
||||
button-class="button-animation flex flex-col gap-1 px-6 py-4 w-full bg-transparent cursor-pointer border-none"
|
||||
content-class="mb-4 mx-3"
|
||||
inner-panel-class="p-1"
|
||||
:open-by-default="
|
||||
![
|
||||
'server_category_minecraft_server_meta',
|
||||
'server_category_minecraft_server_community',
|
||||
'server_game_version',
|
||||
'server_status',
|
||||
].includes(filterType.id)
|
||||
"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="m-0 text-lg">{{ filterType.formatted_name }}</h3>
|
||||
</template>
|
||||
</SearchSidebarFilter>
|
||||
</template>
|
||||
<template v-else>
|
||||
<SearchSidebarFilter
|
||||
v-for="filter in filters.filter((f) => f.display !== 'none')"
|
||||
:key="`filter-${filter.id}`"
|
||||
v-model:selected-filters="currentFilters"
|
||||
v-model:toggled-groups="toggledGroups"
|
||||
v-model:overridden-provided-filter-types="overriddenProvidedFilterTypes"
|
||||
:provided-filters="serverFilters"
|
||||
:filter-type="filter"
|
||||
:class="
|
||||
filtersMenuOpen
|
||||
? 'border-0 border-b-[1px] border-solid border-divider last:border-b-0'
|
||||
: 'card-shadow rounded-2xl bg-bg-raised'
|
||||
"
|
||||
button-class="button-animation flex flex-col gap-1 px-6 py-4 w-full bg-transparent cursor-pointer border-none"
|
||||
content-class="mb-4 mx-3"
|
||||
inner-panel-class="p-1"
|
||||
:open-by-default="!(currentType === 'shader' && filter.id === 'game_version')"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="m-0 text-lg">{{ filter.formatted_name }}</h3>
|
||||
</template>
|
||||
<template v-if="currentType === 'shader' && filter.id === 'game_version'" #prefix>
|
||||
<div class="mb-4 grid grid-cols-[auto_1fr] gap-2 px-3 text-sm font-medium text-blue">
|
||||
<InfoIcon class="mt-1 size-4" />
|
||||
<span> {{ formatMessage(messages.gameVersionShaderMessage) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #locked-game_version>
|
||||
{{ formatMessage(messages.gameVersionProvidedByServer) }}
|
||||
</template>
|
||||
<template #locked-mod_loader>
|
||||
{{ formatMessage(messages.modLoaderProvidedByServer) }}
|
||||
</template>
|
||||
<template #sync-button> {{ formatMessage(messages.syncFilterButton) }}</template>
|
||||
</SearchSidebarFilter>
|
||||
</template>
|
||||
<SearchSidebarFilter
|
||||
v-for="filter in filters.filter((f) => f.display !== 'none')"
|
||||
:key="`filter-${filter.id}`"
|
||||
v-model:selected-filters="currentFilters"
|
||||
v-model:toggled-groups="toggledGroups"
|
||||
v-model:overridden-provided-filter-types="overriddenProvidedFilterTypes"
|
||||
:provided-filters="serverFilters"
|
||||
:filter-type="filter"
|
||||
:class="
|
||||
filtersMenuOpen
|
||||
? 'border-0 border-b-[1px] border-solid border-divider last:border-b-0'
|
||||
: 'card-shadow rounded-2xl bg-bg-raised'
|
||||
"
|
||||
button-class="button-animation flex flex-col gap-1 px-6 py-4 w-full bg-transparent cursor-pointer border-none"
|
||||
content-class="mb-4 mx-3"
|
||||
inner-panel-class="p-1"
|
||||
:open-by-default="!(currentType === 'shader' && filter.id === 'game_version')"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="m-0 text-lg">{{ filter.formatted_name }}</h3>
|
||||
</template>
|
||||
<template v-if="currentType === 'shader' && filter.id === 'game_version'" #prefix>
|
||||
<div class="mb-4 grid grid-cols-[auto_1fr] gap-2 px-3 text-sm font-medium text-blue">
|
||||
<InfoIcon class="mt-1 size-4" />
|
||||
<span> {{ formatMessage(messages.gameVersionShaderMessage) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #locked-game_version>
|
||||
{{ formatMessage(messages.gameVersionProvidedByServer) }}
|
||||
</template>
|
||||
<template #locked-mod_loader>
|
||||
{{ formatMessage(messages.modLoaderProvidedByServer) }}
|
||||
</template>
|
||||
<template #sync-button> {{ formatMessage(messages.syncFilterButton) }}</template>
|
||||
</SearchSidebarFilter>
|
||||
</div>
|
||||
</aside>
|
||||
<section class="normal-page__content">
|
||||
@@ -727,10 +786,10 @@ const getServerModpackContent = (hit: Labrinth.Search.v3.ResultSearchProject) =>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="effectiveSortType"
|
||||
v-model="currentSortType"
|
||||
class="!w-auto flex-grow md:flex-grow-0"
|
||||
name="Sort by"
|
||||
:options="effectiveSortTypes"
|
||||
:options="[...sortTypes]"
|
||||
:display-name="(option?: SortType) => option?.display"
|
||||
@change="updateSearchResults()"
|
||||
>
|
||||
@@ -776,14 +835,6 @@ const getServerModpackContent = (hit: Labrinth.Search.v3.ResultSearchProject) =>
|
||||
/>
|
||||
</div>
|
||||
<SearchFilterControl
|
||||
v-if="currentType === 'server'"
|
||||
v-model:selected-filters="serverCurrentFilters"
|
||||
:filters="serverFilterTypes"
|
||||
:provided-filters="[]"
|
||||
:overridden-provided-filter-types="[]"
|
||||
/>
|
||||
<SearchFilterControl
|
||||
v-else
|
||||
v-model:selected-filters="currentFilters"
|
||||
:filters="filters.filter((f) => f.display !== 'none')"
|
||||
:provided-filters="serverFilters"
|
||||
@@ -791,14 +842,7 @@ const getServerModpackContent = (hit: Labrinth.Search.v3.ResultSearchProject) =>
|
||||
:provided-message="messages.providedByServer"
|
||||
/>
|
||||
<LogoAnimated v-if="searchLoading && !noLoad" />
|
||||
<div
|
||||
v-else-if="
|
||||
currentType === 'server'
|
||||
? serverHits.length === 0
|
||||
: results && results.hits && results.hits.length === 0
|
||||
"
|
||||
class="no-results"
|
||||
>
|
||||
<div v-else-if="results && results.hits && results.hits.length === 0" class="no-results">
|
||||
<p>No results found for your query!</p>
|
||||
</div>
|
||||
<div v-else class="search-results-container">
|
||||
@@ -808,37 +852,8 @@ const getServerModpackContent = (hit: Labrinth.Search.v3.ResultSearchProject) =>
|
||||
resultsDisplayMode === 'grid' || resultsDisplayMode === 'gallery' ? 'grid' : 'list'
|
||||
"
|
||||
>
|
||||
<template v-if="currentType === 'server'">
|
||||
<template v-for="result in results?.hits" :key="result.project_id">
|
||||
<ProjectCard
|
||||
v-for="project in serverHits"
|
||||
:key="`server-card-${project.project_id}`"
|
||||
:title="project.name"
|
||||
:icon-url="project.icon_url || undefined"
|
||||
:summary="project.summary"
|
||||
:tags="project.categories"
|
||||
:link="`/server/${project.slug}`"
|
||||
:server-online-players="
|
||||
project.minecraft_java_server?.ping?.data?.players_online ?? 0
|
||||
"
|
||||
:server-recent-plays="project.minecraft_java_server?.verified_plays_2w ?? 0"
|
||||
:server-region="project.minecraft_server?.region"
|
||||
:server-status-online="!!project.minecraft_java_server?.ping?.data"
|
||||
:server-modpack-content="getServerModpackContent(project)"
|
||||
:layout="
|
||||
resultsDisplayMode === 'grid' || resultsDisplayMode === 'gallery' ? 'grid' : 'list'
|
||||
"
|
||||
:max-tags="2"
|
||||
is-server-project
|
||||
exclude-loaders
|
||||
@mouseenter="handleServerProjectMouseEnter(project)"
|
||||
@mouseleave="handleProjectHoverEnd"
|
||||
>
|
||||
</ProjectCard>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ProjectCard
|
||||
v-for="result in results?.hits"
|
||||
:key="result.project_id"
|
||||
:link="`/${projectType?.id ?? 'project'}/${result.slug ? result.slug : result.project_id}`"
|
||||
:title="result.title"
|
||||
:icon-url="result.icon_url"
|
||||
@@ -858,8 +873,8 @@ const getServerModpackContent = (hit: Labrinth.Search.v3.ResultSearchProject) =>
|
||||
:environment="
|
||||
['mod', 'modpack'].includes(currentType)
|
||||
? {
|
||||
clientSide: result.client_side as Labrinth.Projects.v2.Environment,
|
||||
serverSide: result.server_side as Labrinth.Projects.v2.Environment,
|
||||
clientSide: result.client_side,
|
||||
serverSide: result.server_side,
|
||||
}
|
||||
: undefined
|
||||
"
|
||||
@@ -869,7 +884,7 @@ const getServerModpackContent = (hit: Labrinth.Search.v3.ResultSearchProject) =>
|
||||
@mouseenter="handleProjectMouseEnter(result)"
|
||||
@mouseleave="handleProjectHoverEnd"
|
||||
>
|
||||
<template v-if="flags.showDiscoverProjectButtons || server" #actions>
|
||||
<template v-if="flags.showDiscoverProjectButtons || serverData" #actions>
|
||||
<template v-if="flags.showDiscoverProjectButtons">
|
||||
<ButtonStyled color="brand">
|
||||
<button>
|
||||
@@ -893,16 +908,16 @@ const getServerModpackContent = (hit: Labrinth.Search.v3.ResultSearchProject) =>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else-if="server">
|
||||
<template v-else-if="serverData">
|
||||
<ButtonStyled color="brand" type="outlined">
|
||||
<button
|
||||
v-if="
|
||||
(result as InstallableSearchResult).installed ||
|
||||
(server?.content?.data &&
|
||||
server.content.data.find(
|
||||
(x: InstallableMod) => x.project_id === result.project_id,
|
||||
(serverContentData &&
|
||||
(serverContentData.addons ?? []).find(
|
||||
(x) => x.project_id === result.project_id,
|
||||
)) ||
|
||||
server.general?.project?.id === result.project_id
|
||||
serverData.upstream?.project_id === result.project_id
|
||||
"
|
||||
disabled
|
||||
>
|
||||
@@ -933,6 +948,18 @@ const getServerModpackContent = (hit: Labrinth.Search.v3.ResultSearchProject) =>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CreationFlowModal
|
||||
v-if="currentServerId && projectType?.id === 'modpack'"
|
||||
ref="onboardingModalRef"
|
||||
:type="fromContext === 'reset-server' ? 'reset-server' : 'server-onboarding'"
|
||||
:available-loaders="['vanilla', 'fabric', 'neoforge', 'forge', 'quilt', 'paper', 'purpur']"
|
||||
:show-snapshot-toggle="true"
|
||||
:on-back="onOnboardingBack"
|
||||
@hide="onOnboardingHide"
|
||||
@browse-modpacks="() => {}"
|
||||
@create="onModpackFlowCreate"
|
||||
/>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.normal-page__content {
|
||||
|
||||
Reference in New Issue
Block a user