feat: clean up browse shared layout logic + introduce queuing (#6030)
* feat: clean up edge case behaviour and add queued to install logic * fix: remove version choice modal * feat: queued flow * feat: standardize headers in app on proj pages * fix: clear btn * feat: installing floating popup * fix: lint * fix: onboarding/reset logic change for modpacks * qa: big ol qa * fix: lint * fix: lint --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -1,33 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
ServersManageContentPage,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { server, serverId, worldId } = injectModrinthServerContext()
|
||||
const queryClient = useQueryClient()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
if (worldId.value) {
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'servers.manage.content.title',
|
||||
defaultMessage: 'Content - {serverName} - Modrinth',
|
||||
},
|
||||
})
|
||||
|
||||
async function getContentWorldId() {
|
||||
if (worldId.value) return worldId.value
|
||||
|
||||
const serverFull = await queryClient.ensureQueryData({
|
||||
queryKey: ['servers', 'v1', 'detail', serverId],
|
||||
queryFn: () => client.archon.servers_v1.get(serverId),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
const activeWorld = serverFull.worlds.find((world) => world.is_active)
|
||||
return activeWorld?.id ?? serverFull.worlds[0]?.id ?? null
|
||||
}
|
||||
|
||||
const contentWorldId = await getContentWorldId()
|
||||
|
||||
if (contentWorldId) {
|
||||
try {
|
||||
await queryClient.ensureQueryData({
|
||||
const content = await queryClient.ensureQueryData({
|
||||
queryKey: ['content', 'list', 'v1', serverId],
|
||||
queryFn: () =>
|
||||
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
|
||||
client.archon.content_v1.getAddons(serverId, contentWorldId, { from_modpack: false }),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const modpackProjectId =
|
||||
content.modpack?.spec.platform === 'modrinth' ? content.modpack.spec.project_id : null
|
||||
|
||||
if (modpackProjectId) {
|
||||
await queryClient.ensureQueryData({
|
||||
queryKey: ['labrinth', 'project', modpackProjectId],
|
||||
queryFn: () => client.labrinth.projects_v2.get(modpackProjectId),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// Let mounted layouts' useQuery surface errors; do not fail route setup.
|
||||
}
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: `Content - ${server.value?.name ?? 'Server'} - Modrinth`,
|
||||
title: () =>
|
||||
formatMessage(messages.title, {
|
||||
serverName: server.value?.name ?? formatMessage(commonMessages.serverLabel),
|
||||
}),
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ServersManageContentPage />
|
||||
<ServersManageContentPage :owner-avatar-url-base="''" />
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user