|
|
|
|
@@ -11,20 +11,37 @@ import {
|
|
|
|
|
MoreVerticalIcon,
|
|
|
|
|
SpinnerIcon,
|
|
|
|
|
} from '@modrinth/assets'
|
|
|
|
|
import type { CardAction, CreationFlowContextValue } from '@modrinth/ui'
|
|
|
|
|
import type {
|
|
|
|
|
BrowseInstallContentType,
|
|
|
|
|
BrowseInstallPlan,
|
|
|
|
|
CardAction,
|
|
|
|
|
CreationFlowContextValue,
|
|
|
|
|
PendingServerContentInstall,
|
|
|
|
|
PendingServerContentInstallType,
|
|
|
|
|
} from '@modrinth/ui'
|
|
|
|
|
import {
|
|
|
|
|
addPendingServerContentInstalls,
|
|
|
|
|
BrowseInstallHeader,
|
|
|
|
|
BrowsePageLayout,
|
|
|
|
|
BrowseSidebar,
|
|
|
|
|
commonMessages,
|
|
|
|
|
CreationFlowModal,
|
|
|
|
|
defineMessages,
|
|
|
|
|
flushInstallQueue,
|
|
|
|
|
getTargetInstallPreferences,
|
|
|
|
|
injectModrinthClient,
|
|
|
|
|
injectNotificationManager,
|
|
|
|
|
PROJECT_DEP_MARKER_QUERY,
|
|
|
|
|
provideBrowseManager,
|
|
|
|
|
readPendingServerContentInstalls,
|
|
|
|
|
removePendingServerContentInstall,
|
|
|
|
|
requestInstall,
|
|
|
|
|
SelectedProjectsFloatingBar,
|
|
|
|
|
useBrowseSearch,
|
|
|
|
|
useDebugLogger,
|
|
|
|
|
useStickyObserver,
|
|
|
|
|
useVIntl,
|
|
|
|
|
writePendingServerContentInstallBaseline,
|
|
|
|
|
} from '@modrinth/ui'
|
|
|
|
|
import { cycleValue } from '@modrinth/utils'
|
|
|
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
|
|
|
|
@@ -47,6 +64,9 @@ const client = injectModrinthClient()
|
|
|
|
|
const queryClient = useQueryClient()
|
|
|
|
|
|
|
|
|
|
const filtersMenuOpen = ref(false)
|
|
|
|
|
const stickyInstallHeaderRef = ref<HTMLElement | null>(null)
|
|
|
|
|
|
|
|
|
|
useStickyObserver(stickyInstallHeaderRef, 'DiscoverInstallHeader')
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
|
|
|
|
@@ -55,7 +75,7 @@ const tags = useGeneratedState()
|
|
|
|
|
const flags = useFeatureFlags()
|
|
|
|
|
const auth = await useAuth()
|
|
|
|
|
|
|
|
|
|
const { handleError } = injectNotificationManager()
|
|
|
|
|
const { addNotification, handleError } = injectNotificationManager()
|
|
|
|
|
|
|
|
|
|
let prefetchTimeout: ReturnType<typeof useTimeoutFn> | null = null
|
|
|
|
|
const HOVER_DURATION_TO_PREFETCH_MS = 500
|
|
|
|
|
@@ -143,7 +163,7 @@ function cycleSearchDisplayMode() {
|
|
|
|
|
|
|
|
|
|
const currentServerId = computed(() => queryAsString(route.query.sid) || null)
|
|
|
|
|
const fromContext = computed(() => queryAsString(route.query.from) || null)
|
|
|
|
|
const currentWorldId = computed(() => queryAsString(route.query.wid) || undefined)
|
|
|
|
|
const currentWorldId = computed(() => queryAsString(route.query.wid) || null)
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
data: serverData,
|
|
|
|
|
@@ -176,11 +196,139 @@ const serverIcon = computed(() => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const serverHideInstalled = ref(false)
|
|
|
|
|
const hideSelectedServerInstalls = ref(false)
|
|
|
|
|
const installingProjectIds = ref<Set<string>>(new Set())
|
|
|
|
|
const optimisticallyInstalledProjectIds = ref<Set<string>>(new Set())
|
|
|
|
|
const hiddenInstalledProjectIds = ref<Set<string>>(new Set())
|
|
|
|
|
const hiddenInstalledProjectIdsInitialized = ref(false)
|
|
|
|
|
|
|
|
|
|
interface InstallableSearchResult extends Labrinth.Search.v2.ResultSearchProject {
|
|
|
|
|
installed?: boolean
|
|
|
|
|
}
|
|
|
|
|
type PendingServerContentInstallInput = Omit<PendingServerContentInstall, 'createdAt'>
|
|
|
|
|
|
|
|
|
|
const queuedServerInstalls = ref<Map<string, BrowseInstallPlan<InstallableSearchResult>>>(new Map())
|
|
|
|
|
const queuedServerInstallProjectIds = computed(() => new Set(queuedServerInstalls.value.keys()))
|
|
|
|
|
const queuedServerInstallCount = computed(() => queuedServerInstalls.value.size)
|
|
|
|
|
const selectedServerInstallProjects = computed(() =>
|
|
|
|
|
Array.from(queuedServerInstalls.value.values()).map((plan) => ({
|
|
|
|
|
id: plan.projectId,
|
|
|
|
|
name: plan.project.title ?? formatMessage(commonMessages.projectLabel),
|
|
|
|
|
iconUrl: plan.project.icon_url ?? null,
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
const isInstallingQueuedServerInstalls = ref(false)
|
|
|
|
|
const queuedInstallProgress = ref({ completed: 0, total: 0 })
|
|
|
|
|
const serverInstallQueue = {
|
|
|
|
|
get: () => queuedServerInstalls.value,
|
|
|
|
|
set: (plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>) => {
|
|
|
|
|
queuedServerInstalls.value = plans
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getQueuedInstallOwnerFallback(project: InstallableSearchResult) {
|
|
|
|
|
if (project.organization) {
|
|
|
|
|
const ownerId = project.organization_id ?? project.organization
|
|
|
|
|
return {
|
|
|
|
|
id: ownerId,
|
|
|
|
|
name: project.organization,
|
|
|
|
|
type: 'organization' as const,
|
|
|
|
|
link: `/organization/${ownerId}`,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!project.author) return null
|
|
|
|
|
|
|
|
|
|
const ownerId = project.author_id ?? project.author
|
|
|
|
|
return {
|
|
|
|
|
id: ownerId,
|
|
|
|
|
name: project.author,
|
|
|
|
|
type: 'user' as const,
|
|
|
|
|
link: `/user/${ownerId}`,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getQueuedInstallOwner(project: InstallableSearchResult) {
|
|
|
|
|
const fallback = getQueuedInstallOwnerFallback(project)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (project.organization) {
|
|
|
|
|
const organization = await client.labrinth.projects_v3.getOrganization(project.project_id)
|
|
|
|
|
if (organization) {
|
|
|
|
|
return {
|
|
|
|
|
id: organization.id,
|
|
|
|
|
name: organization.name,
|
|
|
|
|
type: 'organization' as const,
|
|
|
|
|
avatar_url: organization.icon_url ?? undefined,
|
|
|
|
|
link: `/organization/${organization.slug}`,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const members = await client.labrinth.projects_v3.getMembers(project.project_id)
|
|
|
|
|
const owner =
|
|
|
|
|
members.find((member) => member.user.id === project.author_id)?.user ??
|
|
|
|
|
members.find((member) => member.is_owner || member.role === 'Owner')?.user ??
|
|
|
|
|
members[0]?.user
|
|
|
|
|
|
|
|
|
|
if (owner) {
|
|
|
|
|
return {
|
|
|
|
|
id: owner.id,
|
|
|
|
|
name: owner.username,
|
|
|
|
|
type: 'user' as const,
|
|
|
|
|
avatar_url: owner.avatar_url,
|
|
|
|
|
link: `/user/${owner.username}`,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
return fallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getQueuedAddonInstallPlans(
|
|
|
|
|
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
|
|
|
|
) {
|
|
|
|
|
return Array.from(plans.values()).filter((plan) => plan.contentType !== 'modpack')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getQueuedInstallPlaceholder(
|
|
|
|
|
plan: BrowseInstallPlan<InstallableSearchResult>,
|
|
|
|
|
owner: PendingServerContentInstallInput['owner'],
|
|
|
|
|
): PendingServerContentInstallInput {
|
|
|
|
|
return {
|
|
|
|
|
projectId: plan.projectId,
|
|
|
|
|
versionId: plan.versionId,
|
|
|
|
|
contentType: plan.contentType as PendingServerContentInstallType,
|
|
|
|
|
title: plan.project.title ?? formatMessage(commonMessages.projectLabel),
|
|
|
|
|
versionName: plan.versionName ?? null,
|
|
|
|
|
versionNumber: plan.versionNumber ?? null,
|
|
|
|
|
fileName: plan.fileName ?? null,
|
|
|
|
|
owner,
|
|
|
|
|
slug: plan.project.slug ?? plan.projectId,
|
|
|
|
|
iconUrl: plan.project.icon_url ?? null,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getQueuedInstallPlaceholderFallbacks(
|
|
|
|
|
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
|
|
|
|
) {
|
|
|
|
|
return getQueuedAddonInstallPlans(plans).map((plan) =>
|
|
|
|
|
getQueuedInstallPlaceholder(plan, getQueuedInstallOwnerFallback(plan.project)),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getQueuedInstallPlaceholders(
|
|
|
|
|
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
|
|
|
|
) {
|
|
|
|
|
return Promise.all(
|
|
|
|
|
getQueuedAddonInstallPlans(plans).map(async (plan) =>
|
|
|
|
|
getQueuedInstallPlaceholder(plan, await getQueuedInstallOwner(plan.project)),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setProjectInstalling(projectId: string, installing: boolean) {
|
|
|
|
|
const next = new Set(installingProjectIds.value)
|
|
|
|
|
if (installing) {
|
|
|
|
|
@@ -206,6 +354,10 @@ function getServerInstalledProjectIds(data = serverContentData.value) {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getServerInstalledContentKeys(data = serverContentData.value) {
|
|
|
|
|
return new Set((data?.addons ?? []).map((addon) => addon.project_id ?? addon.filename))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function syncHiddenInstalledProjectIds() {
|
|
|
|
|
hiddenInstalledProjectIds.value = new Set([
|
|
|
|
|
...getServerInstalledProjectIds(),
|
|
|
|
|
@@ -242,21 +394,22 @@ watch(
|
|
|
|
|
const installContentMutation = useMutation({
|
|
|
|
|
mutationFn: ({
|
|
|
|
|
serverId,
|
|
|
|
|
worldId,
|
|
|
|
|
projectId,
|
|
|
|
|
versionId,
|
|
|
|
|
}: {
|
|
|
|
|
serverId: string
|
|
|
|
|
worldId: string
|
|
|
|
|
projectId: string
|
|
|
|
|
versionId: string
|
|
|
|
|
}) =>
|
|
|
|
|
client.archon.content_v1.addAddon(serverId, currentWorldId.value!, {
|
|
|
|
|
client.archon.content_v1.addAddon(serverId, worldId, {
|
|
|
|
|
project_id: projectId,
|
|
|
|
|
version_id: versionId,
|
|
|
|
|
}),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
if (currentServerId.value) {
|
|
|
|
|
queryClient.refetchQueries({ queryKey: ['content', 'list', currentServerId.value] })
|
|
|
|
|
}
|
|
|
|
|
onSuccess: (_data, variables) => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', variables.serverId] })
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@@ -309,6 +462,16 @@ const serverFilters = computed(() => {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hideSelectedServerInstalls.value && queuedServerInstallProjectIds.value.size > 0) {
|
|
|
|
|
for (const id of queuedServerInstallProjectIds.value) {
|
|
|
|
|
filters.push({
|
|
|
|
|
type: 'project_id',
|
|
|
|
|
option: `project_id:${id}`,
|
|
|
|
|
negative: true,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentServerId.value && projectType.value?.id === 'modpack') {
|
|
|
|
|
@@ -321,78 +484,199 @@ const serverFilters = computed(() => {
|
|
|
|
|
return filters
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
interface InstallableSearchResult extends Labrinth.Search.v2.ResultSearchProject {
|
|
|
|
|
installed?: boolean
|
|
|
|
|
function getCurrentServerInstallType(): BrowseInstallContentType {
|
|
|
|
|
const type = projectType.value?.id
|
|
|
|
|
if (type === 'modpack' || type === 'mod' || type === 'plugin' || type === 'datapack') {
|
|
|
|
|
return type
|
|
|
|
|
}
|
|
|
|
|
throw new Error(formatMessage(messages.unsupportedContentType))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getServerInstallTargetPreferences(contentType: BrowseInstallContentType) {
|
|
|
|
|
return getTargetInstallPreferences(
|
|
|
|
|
{
|
|
|
|
|
gameVersion: serverData.value?.mc_version,
|
|
|
|
|
loader: serverData.value?.loader,
|
|
|
|
|
},
|
|
|
|
|
contentType,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getInstallProjectVersions(projectId: string) {
|
|
|
|
|
return client.labrinth.versions_v2.getProjectVersions(projectId, {
|
|
|
|
|
include_changelog: false,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearQueuedServerInstalls() {
|
|
|
|
|
queuedServerInstalls.value = new Map()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeQueuedServerInstall(projectId: string) {
|
|
|
|
|
const nextPlans = new Map(queuedServerInstalls.value)
|
|
|
|
|
nextPlans.delete(projectId)
|
|
|
|
|
queuedServerInstalls.value = nextPlans
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch([currentServerId, currentWorldId], ([serverId, worldId], [prevServerId, prevWorldId]) => {
|
|
|
|
|
if (serverId !== prevServerId || worldId !== prevWorldId) {
|
|
|
|
|
clearQueuedServerInstalls()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
async function flushQueuedServerInstalls(
|
|
|
|
|
serverId: string | null = currentServerId.value,
|
|
|
|
|
worldId: string | null = currentWorldId.value,
|
|
|
|
|
) {
|
|
|
|
|
if (queuedServerInstalls.value.size === 0) return true
|
|
|
|
|
if (isInstallingQueuedServerInstalls.value) return false
|
|
|
|
|
|
|
|
|
|
if (!serverId || !worldId) {
|
|
|
|
|
handleError(new Error(formatMessage(messages.noServerWorld)))
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isInstallingQueuedServerInstalls.value = true
|
|
|
|
|
queuedInstallProgress.value = {
|
|
|
|
|
completed: 0,
|
|
|
|
|
total: queuedServerInstalls.value.size,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await flushInstallQueue({
|
|
|
|
|
queue: serverInstallQueue,
|
|
|
|
|
install: async (plan) => {
|
|
|
|
|
await installContentMutation.mutateAsync({
|
|
|
|
|
serverId,
|
|
|
|
|
worldId,
|
|
|
|
|
projectId: plan.projectId,
|
|
|
|
|
versionId: plan.versionId,
|
|
|
|
|
})
|
|
|
|
|
markProjectInstalled(plan.projectId)
|
|
|
|
|
},
|
|
|
|
|
onError: (error, plan) => {
|
|
|
|
|
removePendingServerContentInstall(serverId, worldId, plan.projectId)
|
|
|
|
|
handleError(error as Error)
|
|
|
|
|
},
|
|
|
|
|
onProgress: (completed, total) => {
|
|
|
|
|
queuedInstallProgress.value = { completed, total }
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return result.ok
|
|
|
|
|
} finally {
|
|
|
|
|
isInstallingQueuedServerInstalls.value = false
|
|
|
|
|
queuedInstallProgress.value = { completed: 0, total: 0 }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function discardQueuedServerInstallsAndBack() {
|
|
|
|
|
clearQueuedServerInstalls()
|
|
|
|
|
await navigateTo(serverBackUrl.value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function installQueuedServerInstallsAndBack() {
|
|
|
|
|
const sid = currentServerId.value
|
|
|
|
|
const wid = currentWorldId.value
|
|
|
|
|
const backUrl = serverBackUrl.value
|
|
|
|
|
const plans = new Map(queuedServerInstalls.value)
|
|
|
|
|
|
|
|
|
|
if (sid && wid) {
|
|
|
|
|
writePendingServerContentInstallBaseline(sid, wid, [
|
|
|
|
|
...getServerInstalledContentKeys(),
|
|
|
|
|
...optimisticallyInstalledProjectIds.value,
|
|
|
|
|
])
|
|
|
|
|
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
|
|
|
|
|
void getQueuedInstallPlaceholders(plans)
|
|
|
|
|
.then((items) => {
|
|
|
|
|
const pendingProjectIds = new Set(
|
|
|
|
|
readPendingServerContentInstalls(sid, wid).map((item) => item.projectId),
|
|
|
|
|
)
|
|
|
|
|
addPendingServerContentInstalls(
|
|
|
|
|
sid,
|
|
|
|
|
wid,
|
|
|
|
|
items.filter((item) => pendingProjectIds.has(item.projectId)),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => handleError(err as Error))
|
|
|
|
|
}
|
|
|
|
|
await navigateTo(backUrl)
|
|
|
|
|
|
|
|
|
|
const ok = await flushQueuedServerInstalls(sid, wid)
|
|
|
|
|
if (!ok) {
|
|
|
|
|
queuedServerInstalls.value = new Map()
|
|
|
|
|
addNotification({
|
|
|
|
|
type: 'error',
|
|
|
|
|
title: formatMessage(messages.someProjectsFailedTitle),
|
|
|
|
|
text: formatMessage(messages.someProjectsFailedText),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function serverInstall(project: InstallableSearchResult) {
|
|
|
|
|
if (!serverData.value || !currentServerId.value) {
|
|
|
|
|
if (!serverData.value || !currentServerId.value || !currentWorldId.value) {
|
|
|
|
|
handleError(new Error('No server to install to.'))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
setProjectInstalling(project.project_id, true)
|
|
|
|
|
const contentType = getCurrentServerInstallType()
|
|
|
|
|
const isModpack = contentType === 'modpack'
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (projectType.value?.id === 'modpack') {
|
|
|
|
|
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'))
|
|
|
|
|
setProjectInstalling(project.project_id, false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const modalInstance = onboardingModalRef.value
|
|
|
|
|
if (modalInstance) {
|
|
|
|
|
onboardingInstallingProject.value = project
|
|
|
|
|
if (!isModpack && queuedServerInstallProjectIds.value.has(project.project_id)) {
|
|
|
|
|
removeQueuedServerInstall(project.project_id)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isModpack || !queuedServerInstallProjectIds.value.has(project.project_id)) {
|
|
|
|
|
setProjectInstalling(project.project_id, true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await requestInstall({
|
|
|
|
|
project,
|
|
|
|
|
contentType,
|
|
|
|
|
mode: isModpack ? 'immediate' : 'queue',
|
|
|
|
|
selectedFilters: isModpack ? [] : searchState.currentFilters.value,
|
|
|
|
|
providedFilters: isModpack ? [] : serverFilters.value,
|
|
|
|
|
overriddenProvidedFilterTypes: isModpack
|
|
|
|
|
? []
|
|
|
|
|
: searchState.overriddenProvidedFilterTypes.value,
|
|
|
|
|
targetPreferences: getServerInstallTargetPreferences(contentType),
|
|
|
|
|
getProjectVersions: getInstallProjectVersions,
|
|
|
|
|
queue: serverInstallQueue,
|
|
|
|
|
install: async (plan) => {
|
|
|
|
|
const modalInstance = onboardingModalRef.value
|
|
|
|
|
if (!modalInstance) {
|
|
|
|
|
setProjectInstalling(plan.projectId, false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onboardingInstallingProject.value = plan.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,
|
|
|
|
|
projectId: plan.projectId,
|
|
|
|
|
versionId: plan.versionId,
|
|
|
|
|
name: plan.project.title,
|
|
|
|
|
iconUrl: plan.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}`,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
setProjectInstalling(project.project_id, false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
await installContentMutation.mutateAsync({
|
|
|
|
|
serverId: currentServerId.value,
|
|
|
|
|
projectId: version.project_id,
|
|
|
|
|
versionId: version.id,
|
|
|
|
|
})
|
|
|
|
|
markProjectInstalled(project.project_id)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e)
|
|
|
|
|
handleError(new Error(`Error installing content ${e}`))
|
|
|
|
|
if (isModpack) {
|
|
|
|
|
setProjectInstalling(project.project_id, false)
|
|
|
|
|
}
|
|
|
|
|
handleError(e instanceof Error ? e : new Error(`Error installing content ${e}`))
|
|
|
|
|
} finally {
|
|
|
|
|
if (!isModpack) {
|
|
|
|
|
setProjectInstalling(project.project_id, false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
setProjectInstalling(project.project_id, false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getServerModpackContent(project: Labrinth.Search.v3.ResultSearchProject) {
|
|
|
|
|
@@ -503,6 +787,7 @@ function getCardActions(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (serverData.value) {
|
|
|
|
|
const isQueued = queuedServerInstallProjectIds.value.has(result.project_id)
|
|
|
|
|
const isInstalled =
|
|
|
|
|
projectResult.installed ||
|
|
|
|
|
optimisticallyInstalledProjectIds.value.has(result.project_id) ||
|
|
|
|
|
@@ -510,15 +795,36 @@ function getCardActions(
|
|
|
|
|
(serverContentData.value.addons ?? []).find((x) => x.project_id === result.project_id)) ||
|
|
|
|
|
serverData.value.upstream?.project_id === result.project_id
|
|
|
|
|
const isInstalling = installingProjectIds.value.has(result.project_id)
|
|
|
|
|
const isInstallingSelection = isInstallingQueuedServerInstalls.value
|
|
|
|
|
const validatingInstall =
|
|
|
|
|
isInstalling && currentProjectType !== 'modpack' && !isInstallingSelection
|
|
|
|
|
const installLabel = isInstalled
|
|
|
|
|
? formatMessage(commonMessages.installedLabel)
|
|
|
|
|
: isQueued
|
|
|
|
|
? isInstalling || isInstallingSelection
|
|
|
|
|
? validatingInstall
|
|
|
|
|
? formatMessage(commonMessages.validatingLabel)
|
|
|
|
|
: formatMessage(commonMessages.installingLabel)
|
|
|
|
|
: formatMessage(commonMessages.selectedLabel)
|
|
|
|
|
: isInstalling || isInstallingSelection
|
|
|
|
|
? validatingInstall
|
|
|
|
|
? formatMessage(commonMessages.validatingLabel)
|
|
|
|
|
: formatMessage(commonMessages.installingLabel)
|
|
|
|
|
: formatMessage(commonMessages.installButton)
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
key: 'install',
|
|
|
|
|
label: isInstalling ? 'Installing...' : isInstalled ? 'Installed' : 'Install',
|
|
|
|
|
icon: isInstalling ? SpinnerIcon : isInstalled ? CheckIcon : DownloadIcon,
|
|
|
|
|
iconClass: isInstalling ? 'animate-spin' : undefined,
|
|
|
|
|
disabled: !!isInstalled || isInstalling,
|
|
|
|
|
color: 'brand',
|
|
|
|
|
label: installLabel,
|
|
|
|
|
icon:
|
|
|
|
|
isInstalling || isInstallingSelection
|
|
|
|
|
? SpinnerIcon
|
|
|
|
|
: isQueued || isInstalled
|
|
|
|
|
? CheckIcon
|
|
|
|
|
: DownloadIcon,
|
|
|
|
|
iconClass: isInstalling || isInstallingSelection ? 'animate-spin' : undefined,
|
|
|
|
|
disabled: !!isInstalled || isInstalling || isInstallingSelection,
|
|
|
|
|
color: isQueued && !isInstalling && !isInstallingSelection ? 'green' : 'brand',
|
|
|
|
|
type: 'outlined',
|
|
|
|
|
onClick: () => serverInstall(projectResult),
|
|
|
|
|
},
|
|
|
|
|
@@ -579,15 +885,15 @@ const serverBackUrl = computed(() => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const serverBackLabel = computed(() => {
|
|
|
|
|
if (fromContext.value === 'onboarding') return 'Back to setup'
|
|
|
|
|
if (fromContext.value === 'reset-server') return 'Cancel reset'
|
|
|
|
|
return 'Back to server'
|
|
|
|
|
if (fromContext.value === 'onboarding') return formatMessage(messages.backToSetup)
|
|
|
|
|
if (fromContext.value === 'reset-server') return formatMessage(messages.cancelReset)
|
|
|
|
|
return formatMessage(messages.backToServer)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const serverBrowseHeading = computed(() =>
|
|
|
|
|
fromContext.value === 'reset-server'
|
|
|
|
|
? 'Select modpack to install after reset'
|
|
|
|
|
: 'Install content to server',
|
|
|
|
|
? formatMessage(messages.resetModpackHeading)
|
|
|
|
|
: formatMessage(commonMessages.installingContentLabel),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const installContext = computed(() => {
|
|
|
|
|
@@ -603,10 +909,51 @@ const installContext = computed(() => {
|
|
|
|
|
backUrl: serverBackUrl.value,
|
|
|
|
|
backLabel: serverBackLabel.value,
|
|
|
|
|
heading: serverBrowseHeading.value,
|
|
|
|
|
queuedCount: queuedServerInstallCount.value,
|
|
|
|
|
selectedProjects: selectedServerInstallProjects.value,
|
|
|
|
|
isInstallingSelected: isInstallingQueuedServerInstalls.value,
|
|
|
|
|
installProgress: queuedInstallProgress.value,
|
|
|
|
|
clearQueued: clearQueuedServerInstalls,
|
|
|
|
|
clearSelected: clearQueuedServerInstalls,
|
|
|
|
|
onBack: flushQueuedServerInstalls,
|
|
|
|
|
discardSelectedAndBack: discardQueuedServerInstallsAndBack,
|
|
|
|
|
installSelected: installQueuedServerInstallsAndBack,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const messages = defineMessages({
|
|
|
|
|
unsupportedContentType: {
|
|
|
|
|
id: 'discover.install.error.unsupported-content-type',
|
|
|
|
|
defaultMessage: 'This content type cannot be installed to a server from browse.',
|
|
|
|
|
},
|
|
|
|
|
noServerWorld: {
|
|
|
|
|
id: 'discover.install.error.no-server-world',
|
|
|
|
|
defaultMessage: 'No server world is available for install.',
|
|
|
|
|
},
|
|
|
|
|
someProjectsFailedTitle: {
|
|
|
|
|
id: 'discover.install.error.some-projects-failed.title',
|
|
|
|
|
defaultMessage: 'Some projects failed to install',
|
|
|
|
|
},
|
|
|
|
|
someProjectsFailedText: {
|
|
|
|
|
id: 'discover.install.error.some-projects-failed.description',
|
|
|
|
|
defaultMessage: 'Failed projects were not added. You can try installing them again.',
|
|
|
|
|
},
|
|
|
|
|
backToSetup: {
|
|
|
|
|
id: 'discover.install.back-to-setup',
|
|
|
|
|
defaultMessage: 'Back to setup',
|
|
|
|
|
},
|
|
|
|
|
cancelReset: {
|
|
|
|
|
id: 'discover.install.cancel-reset',
|
|
|
|
|
defaultMessage: 'Cancel reset',
|
|
|
|
|
},
|
|
|
|
|
backToServer: {
|
|
|
|
|
id: 'discover.install.back-to-server',
|
|
|
|
|
defaultMessage: 'Back to server',
|
|
|
|
|
},
|
|
|
|
|
resetModpackHeading: {
|
|
|
|
|
id: 'discover.install.heading.reset-modpack',
|
|
|
|
|
defaultMessage: 'Selecting modpack to install after reset',
|
|
|
|
|
},
|
|
|
|
|
gameVersionProvidedByServer: {
|
|
|
|
|
id: 'search.filter.locked.server-game-version.title',
|
|
|
|
|
defaultMessage: 'Game version is provided by the server',
|
|
|
|
|
@@ -623,6 +970,21 @@ const messages = defineMessages({
|
|
|
|
|
id: 'search.filter.locked.server.sync',
|
|
|
|
|
defaultMessage: 'Sync with server',
|
|
|
|
|
},
|
|
|
|
|
seoTitle: {
|
|
|
|
|
id: 'discover.seo.title',
|
|
|
|
|
defaultMessage:
|
|
|
|
|
'Search {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}}',
|
|
|
|
|
},
|
|
|
|
|
seoTitleWithQuery: {
|
|
|
|
|
id: 'discover.seo.title-with-query',
|
|
|
|
|
defaultMessage:
|
|
|
|
|
'Search {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}} | {query}',
|
|
|
|
|
},
|
|
|
|
|
seoDescription: {
|
|
|
|
|
id: 'discover.seo.description',
|
|
|
|
|
defaultMessage:
|
|
|
|
|
'Search and browse thousands of Minecraft {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}} on Modrinth with instant, accurate search results. Our filters help you quickly find the best Minecraft {projectType, select, mod {mods} modpack {modpacks} resourcepack {resource packs} shader {shaders} plugin {plugins} datapack {datapacks} other {projects}}.',
|
|
|
|
|
},
|
|
|
|
|
gameVersionShaderMessage: {
|
|
|
|
|
id: 'search.filter.game-version-shader-message',
|
|
|
|
|
defaultMessage:
|
|
|
|
|
@@ -648,6 +1010,12 @@ const searchState = useBrowseSearch({
|
|
|
|
|
displayMode: resultsDisplayMode,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
watch(queuedServerInstallCount, (count) => {
|
|
|
|
|
if (count === 0) {
|
|
|
|
|
hideSelectedServerInstalls.value = false
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() =>
|
|
|
|
|
searchState.isServerType.value
|
|
|
|
|
@@ -673,13 +1041,16 @@ watch(
|
|
|
|
|
debug('calling initial refreshSearch')
|
|
|
|
|
searchState.refreshSearch()
|
|
|
|
|
|
|
|
|
|
const ogTitle = computed(
|
|
|
|
|
() =>
|
|
|
|
|
`Search ${projectType.value?.display ?? 'project'}s${searchState.query.value ? ' | ' + searchState.query.value : ''}`,
|
|
|
|
|
const ogTitle = computed(() =>
|
|
|
|
|
searchState.query.value
|
|
|
|
|
? formatMessage(messages.seoTitleWithQuery, {
|
|
|
|
|
projectType: projectType.value?.id ?? 'project',
|
|
|
|
|
query: searchState.query.value,
|
|
|
|
|
})
|
|
|
|
|
: formatMessage(messages.seoTitle, { projectType: projectType.value?.id ?? 'project' }),
|
|
|
|
|
)
|
|
|
|
|
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 description = computed(() =>
|
|
|
|
|
formatMessage(messages.seoDescription, { projectType: projectType.value?.id ?? 'project' }),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
useSeoMeta({
|
|
|
|
|
@@ -705,7 +1076,15 @@ provideBrowseManager({
|
|
|
|
|
providedFilters: serverFilters,
|
|
|
|
|
hideInstalled: serverHideInstalled,
|
|
|
|
|
showHideInstalled: computed(() => !!serverData.value && projectType.value?.id !== 'modpack'),
|
|
|
|
|
hideInstalledLabel: computed(() => 'Hide already installed content'),
|
|
|
|
|
hideInstalledLabel: computed(() => formatMessage(commonMessages.hideInstalledContentLabel)),
|
|
|
|
|
hideSelected: hideSelectedServerInstalls,
|
|
|
|
|
showHideSelected: computed(
|
|
|
|
|
() =>
|
|
|
|
|
!!serverData.value &&
|
|
|
|
|
projectType.value?.id !== 'modpack' &&
|
|
|
|
|
queuedServerInstallCount.value > 0,
|
|
|
|
|
),
|
|
|
|
|
hideSelectedLabel: computed(() => formatMessage(commonMessages.hideSelectedContentLabel)),
|
|
|
|
|
displayMode: resultsDisplayMode,
|
|
|
|
|
cycleDisplayMode: cycleSearchDisplayMode,
|
|
|
|
|
maxResultsOptions: currentMaxResultsOptions,
|
|
|
|
|
@@ -728,10 +1107,15 @@ provideBrowseManager({
|
|
|
|
|
<Teleport v-if="flags.searchBackground" to="#absolute-background-teleport">
|
|
|
|
|
<div class="search-background"></div>
|
|
|
|
|
</Teleport>
|
|
|
|
|
<div v-if="installContext" class="normal-page__header mb-4 flex flex-col gap-2">
|
|
|
|
|
<div
|
|
|
|
|
v-if="installContext"
|
|
|
|
|
ref="stickyInstallHeaderRef"
|
|
|
|
|
class="normal-page__header browse-install-header-bleed sticky top-0 z-20 mb-4 flex flex-col gap-2 border-0 bg-surface-1 py-3"
|
|
|
|
|
>
|
|
|
|
|
<BrowseInstallHeader />
|
|
|
|
|
</div>
|
|
|
|
|
<aside class="normal-page__sidebar" aria-label="Filters">
|
|
|
|
|
<SelectedProjectsFloatingBar v-if="installContext" :install-context="installContext" />
|
|
|
|
|
<aside class="normal-page__sidebar" :aria-label="formatMessage(commonMessages.filtersLabel)">
|
|
|
|
|
<AdPlaceholder v-if="!auth.user && !serverData" />
|
|
|
|
|
<BrowseSidebar />
|
|
|
|
|
</aside>
|
|
|
|
|
@@ -759,6 +1143,22 @@ provideBrowseManager({
|
|
|
|
|
</section>
|
|
|
|
|
</template>
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.browse-install-header-bleed {
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
margin-inline: -1.5rem;
|
|
|
|
|
padding-inline: 0.75rem !important;
|
|
|
|
|
|
|
|
|
|
&::after {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 50%;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
width: 100vw;
|
|
|
|
|
border-bottom: 1px solid var(--surface-5);
|
|
|
|
|
transform: translateX(50%);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.normal-page__content {
|
|
|
|
|
display: contents;
|
|
|
|
|
|
|
|
|
|
|