mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 10:04:52 +00:00
feat: sync individual content installation states on panel (#6909)
* feat: sync individual content installation states on panel * feat: improve handling * fix: lint * fix: ws connection duplication + disconnecting during browse * fix: sse feats * fix: qa * fix: qa * fix: prepr * fix: bug * fix: lint
This commit is contained in:
@@ -58,9 +58,6 @@ export interface BrowseInstallPlan<TProject extends BrowseInstallProject = Brows
|
||||
project: TProject
|
||||
projectId: string
|
||||
versionId: string
|
||||
versionName?: string
|
||||
versionNumber?: string
|
||||
fileName?: string
|
||||
contentType: BrowseInstallContentType
|
||||
preferences: BrowseInstallPreferences
|
||||
source: BrowseInstallPlanSource
|
||||
@@ -582,15 +579,10 @@ export async function resolveInstallPlan<TProject extends BrowseInstallProject>(
|
||||
const version = getLatestMatchingInstallVersion(versions, candidate.preferences)
|
||||
|
||||
if (version) {
|
||||
const fileName =
|
||||
version.files.find((file) => file.primary)?.filename ?? version.files[0]?.filename
|
||||
return {
|
||||
project: options.project,
|
||||
projectId,
|
||||
versionId: version.id,
|
||||
versionName: version.name,
|
||||
versionNumber: version.version_number,
|
||||
fileName,
|
||||
contentType: options.contentType,
|
||||
preferences: candidate.preferences,
|
||||
source: candidate.source,
|
||||
|
||||
@@ -20,6 +20,7 @@ import BulletDivider from '#ui/components/base/BulletDivider.vue'
|
||||
import type { OverflowMenuOption } from '#ui/components/base/buttons'
|
||||
import { IconButton, TeleportOverflowMenu } from '#ui/components/base/buttons'
|
||||
import Checkbox from '#ui/components/base/Checkbox.vue'
|
||||
import ProgressSpinner from '#ui/components/base/ProgressSpinner.vue'
|
||||
import Toggle from '#ui/components/base/Toggle.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
@@ -61,6 +62,7 @@ interface Props {
|
||||
enabled?: boolean
|
||||
locked?: boolean
|
||||
installing?: boolean
|
||||
installProgress?: number | null
|
||||
hasUpdate?: boolean
|
||||
isClientOnly?: boolean
|
||||
clientWarning?: ClientWarningType | null
|
||||
@@ -87,6 +89,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
enabled: undefined,
|
||||
locked: false,
|
||||
installing: false,
|
||||
installProgress: undefined,
|
||||
hasUpdate: false,
|
||||
isClientOnly: false,
|
||||
clientWarning: null,
|
||||
@@ -139,6 +142,11 @@ const clientWarningMessage = computed(() => {
|
||||
|
||||
const { shift: shiftHeld } = useMagicKeys()
|
||||
const deleteHovered = ref(false)
|
||||
const installTooltip = computed(() => {
|
||||
if (!props.installing) return undefined
|
||||
if (props.installProgress == null) return formatMessage(commonMessages.installingLabel)
|
||||
return `${formatMessage(commonMessages.installingLabel)} (${Math.round(props.installProgress)}%)`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -162,6 +170,7 @@ const deleteHovered = ref(false)
|
||||
v-if="showCheckbox"
|
||||
:model-value="selected ?? false"
|
||||
:aria-label="formatMessage(messages.selectProject, { project: project.title })"
|
||||
:disabled="isDisabled"
|
||||
class="shrink-0"
|
||||
@update:model-value="(value, event) => emit('select', value, event)"
|
||||
/>
|
||||
@@ -170,10 +179,7 @@ const deleteHovered = ref(false)
|
||||
class="flex min-w-0 items-center gap-3 transition-[filter,opacity] duration-200"
|
||||
:class="enabled === false && !disabled ? 'grayscale opacity-50' : ''"
|
||||
>
|
||||
<div
|
||||
v-tooltip="installing ? formatMessage(commonMessages.installingLabel) : undefined"
|
||||
class="relative flex shrink-0 items-center"
|
||||
>
|
||||
<div v-tooltip="installTooltip" class="relative flex shrink-0 items-center">
|
||||
<Avatar
|
||||
:src="project.icon_url"
|
||||
:alt="project.title"
|
||||
@@ -185,7 +191,13 @@ const deleteHovered = ref(false)
|
||||
v-if="installing"
|
||||
class="absolute inset-0 flex items-center justify-center rounded-2xl bg-black/20"
|
||||
>
|
||||
<SpinnerIcon class="size-5 animate-spin text-white" />
|
||||
<ProgressSpinner
|
||||
v-if="installProgress != null && installProgress > 0"
|
||||
:progress="installProgress"
|
||||
:max="100"
|
||||
class="size-5 text-white"
|
||||
/>
|
||||
<SpinnerIcon v-else class="size-5 animate-spin text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-col gap-0.5">
|
||||
|
||||
@@ -104,20 +104,24 @@ defineExpose({
|
||||
})
|
||||
|
||||
// Selection logic
|
||||
const selectableItems = computed(() => props.items.filter((item) => !item.disabled))
|
||||
|
||||
const allSelected = computed(() => {
|
||||
if (props.items.length === 0) return false
|
||||
return props.items.every((item) => selectedIds.value.includes(item.id))
|
||||
if (selectableItems.value.length === 0) return false
|
||||
return selectableItems.value.every((item) => selectedIds.value.includes(item.id))
|
||||
})
|
||||
|
||||
const someSelected = computed(() => {
|
||||
return props.items.some((item) => selectedIds.value.includes(item.id)) && !allSelected.value
|
||||
return (
|
||||
selectableItems.value.some((item) => selectedIds.value.includes(item.id)) && !allSelected.value
|
||||
)
|
||||
})
|
||||
|
||||
function toggleSelectAll() {
|
||||
if (allSelected.value || someSelected.value) {
|
||||
selectedIds.value = []
|
||||
} else {
|
||||
selectedIds.value = props.items.map((item) => item.id)
|
||||
selectedIds.value = selectableItems.value.map((item) => item.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +136,10 @@ function toggleItemSelection(
|
||||
if (selected && event?.shiftKey && lastSelectedIndex.value !== null && index !== undefined) {
|
||||
const start = Math.min(lastSelectedIndex.value, index)
|
||||
const end = Math.max(lastSelectedIndex.value, index)
|
||||
const rangeIds = props.items.slice(start, end + 1).map((item) => item.id)
|
||||
const rangeIds = props.items
|
||||
.slice(start, end + 1)
|
||||
.filter((item) => !item.disabled)
|
||||
.map((item) => item.id)
|
||||
const merged = new Set([...selectedIds.value, ...rangeIds])
|
||||
selectedIds.value = [...merged]
|
||||
} else if (selected) {
|
||||
@@ -192,6 +199,7 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
:model-value="allSelected"
|
||||
:indeterminate="someSelected"
|
||||
:aria-label="formatMessage(commonMessages.selectAllLabel)"
|
||||
:disabled="selectableItems.length === 0"
|
||||
class="shrink-0"
|
||||
@update:model-value="toggleSelectAll"
|
||||
/>
|
||||
@@ -269,6 +277,7 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
:enabled="item.enabled"
|
||||
:locked="item.locked"
|
||||
:installing="item.installing"
|
||||
:install-progress="item.installProgress"
|
||||
:has-update="item.hasUpdate"
|
||||
:is-client-only="item.isClientOnly"
|
||||
:client-warning="item.clientWarning"
|
||||
@@ -336,6 +345,7 @@ function handleSort(column: ContentCardTableSortColumn) {
|
||||
:enabled="item.enabled"
|
||||
:locked="item.locked"
|
||||
:installing="item.installing"
|
||||
:install-progress="item.installProgress"
|
||||
:has-update="item.hasUpdate"
|
||||
:is-client-only="item.isClientOnly"
|
||||
:client-warning="item.clientWarning"
|
||||
|
||||
@@ -434,6 +434,7 @@ const tableItems = computed<ContentCardTableItem[]>(() => {
|
||||
? (ctx.busyMessage?.value ?? null)
|
||||
: base.toggleDisabledTooltip,
|
||||
installing: item.installing === true,
|
||||
installProgress: item.installProgress,
|
||||
hasUpdate: base.hasUpdate ?? item.has_update,
|
||||
isClientOnly: clientWarning !== null,
|
||||
clientWarning,
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface ContentCardTableItem {
|
||||
toggleDisabledTooltip?: string | null
|
||||
hideToggle?: boolean
|
||||
installing?: boolean
|
||||
installProgress?: number | null
|
||||
hasUpdate?: boolean
|
||||
isClientOnly?: boolean
|
||||
clientWarning?: ClientWarningType | null
|
||||
@@ -102,6 +103,7 @@ export interface ContentItem extends Omit<
|
||||
pack_client_retained?: boolean
|
||||
pack_client_depends?: boolean
|
||||
installing?: boolean
|
||||
installProgress?: number | null
|
||||
source_kind?: ContentSourceKind | null
|
||||
external?: boolean
|
||||
external_url?: string
|
||||
|
||||
+12
-2
@@ -67,6 +67,7 @@ export function useInstallationForm(
|
||||
})
|
||||
|
||||
const hasChanges = computed(() => {
|
||||
if (ctx.requiresInstallation?.value) return true
|
||||
if (selectedPlatform.value !== ctx.currentPlatform.value) return true
|
||||
if (selectedGameVersion.value !== ctx.currentGameVersion.value) return true
|
||||
if (
|
||||
@@ -119,8 +120,12 @@ export function useInstallationForm(
|
||||
isValid: isValid.value,
|
||||
hasChanges: hasChanges.value,
|
||||
})
|
||||
if (ctx.isBusy.value) {
|
||||
debug('save: ignored busy')
|
||||
if (ctx.isBusy.value || !isValid.value || !hasChanges.value) {
|
||||
debug('save: ignored', {
|
||||
isBusy: ctx.isBusy.value,
|
||||
isValid: isValid.value,
|
||||
hasChanges: hasChanges.value,
|
||||
})
|
||||
return
|
||||
}
|
||||
isSaving.value = true
|
||||
@@ -209,6 +214,11 @@ export function useInstallationForm(
|
||||
selectedGameVersion: selectedGameVersion.value,
|
||||
selectedLoaderVersion: selectedLoaderVersion.value,
|
||||
})
|
||||
if (!isValid.value) {
|
||||
debug('performSave: ignored invalid form')
|
||||
isSaving.value = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
const loaderVersionId =
|
||||
selectedPlatform.value !== 'vanilla'
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ export interface InstallationSettingsContext {
|
||||
currentPlatform: ComputedRef<string>
|
||||
currentGameVersion: ComputedRef<string>
|
||||
currentLoaderVersion: ComputedRef<string>
|
||||
requiresInstallation?: Ref<boolean> | ComputedRef<boolean>
|
||||
|
||||
availablePlatforms: string[] | ComputedRef<string[]>
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import { RotateCounterClockwiseIcon } from '@modrinth/assets'
|
||||
import {
|
||||
commonMessages,
|
||||
@@ -105,9 +105,19 @@ import { injectFilePicker } from '#ui/providers/file-picker'
|
||||
|
||||
const debug = useDebugLogger('LoaderPage')
|
||||
const client = injectModrinthClient()
|
||||
const { server, serverId, worldId, isSyncingContent, busyReasons } = injectModrinthServerContext()
|
||||
const {
|
||||
beginInstallation,
|
||||
busyReasons,
|
||||
cancelOptimisticInstallation,
|
||||
installation,
|
||||
server,
|
||||
serverId,
|
||||
worldId,
|
||||
} = injectModrinthServerContext()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const queryClient = useQueryClient()
|
||||
const serverDetailQueryKey = ['servers', 'detail', serverId] as const
|
||||
const addonsQueryKey = ['content', 'list', 'v1', serverId] as const
|
||||
const tags = injectTags()
|
||||
const { formatMessage } = useVIntl()
|
||||
const serverSettings = injectServerSettings()
|
||||
@@ -200,19 +210,7 @@ const emit = defineEmits<{
|
||||
'reinstall-failed': []
|
||||
}>()
|
||||
|
||||
const isInstalling = computed(() => {
|
||||
const val =
|
||||
server.value?.status === 'installing' || isSyncingContent.value || busyReasons.value.length > 0
|
||||
debug(
|
||||
'isInstalling:',
|
||||
val,
|
||||
'server.status:',
|
||||
server.value?.status,
|
||||
'isSyncingContent:',
|
||||
isSyncingContent.value,
|
||||
)
|
||||
return val
|
||||
})
|
||||
const isInstalling = computed(() => busyReasons.value.length > 0)
|
||||
const setupActionDisabled = computed(() => !canSetup.value || isInstalling.value)
|
||||
const setupActionDisabledMessage = computed(() => {
|
||||
if (!canSetup.value) return permissionDeniedMessage.value
|
||||
@@ -234,20 +232,24 @@ function showResetServerModal() {
|
||||
async function invalidateServerState() {
|
||||
debug('invalidateServerState: starting')
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', serverId] }),
|
||||
queryClient.invalidateQueries({ queryKey: serverDetailQueryKey }),
|
||||
queryClient.invalidateQueries({ queryKey: addonsQueryKey }),
|
||||
])
|
||||
debug('invalidateServerState: complete')
|
||||
}
|
||||
|
||||
const addonsQuery = useQuery({
|
||||
queryKey: computed(() => ['content', 'list', 'v1', serverId]),
|
||||
queryKey: addonsQueryKey,
|
||||
queryFn: () =>
|
||||
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
|
||||
enabled: computed(() => worldId.value !== null),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const requiresInstallation = computed(
|
||||
() => installation.value?.status === 'failed' || addonsQuery.data.value?.error != null,
|
||||
)
|
||||
|
||||
const modpack = computed(() => addonsQuery.data.value?.modpack ?? null)
|
||||
|
||||
const modpackProjectId = computed(() => {
|
||||
@@ -283,6 +285,8 @@ function showResetToOnboardingModal() {
|
||||
}
|
||||
|
||||
const modLoaders = ['fabric', 'forge', 'quilt', 'neoforge']
|
||||
const loaderGameVersionPlaceholder = '${modrinth.gameVersion}'
|
||||
const minecraftServerDownloadsStartTime = Date.parse('2012-04-04T00:00:00Z')
|
||||
|
||||
function toApiLoaderName(loader: string): string {
|
||||
return loader === 'neoforge' ? 'neo' : loader
|
||||
@@ -291,10 +295,14 @@ function toApiLoaderName(loader: string): string {
|
||||
const apiLoaderName = computed(() =>
|
||||
modLoaders.includes(editingPlatform.value) ? toApiLoaderName(editingPlatform.value) : null,
|
||||
)
|
||||
const manifestFormatVersion = computed(() => (apiLoaderName.value === 'quilt' ? 1 : 0))
|
||||
|
||||
const manifestQuery = useQuery({
|
||||
queryKey: computed(() => ['loader-manifest', apiLoaderName.value] as const),
|
||||
queryFn: () => client.launchermeta.manifest_v0.getManifest(apiLoaderName.value!),
|
||||
queryKey: computed(
|
||||
() => ['loader-manifest', apiLoaderName.value, manifestFormatVersion.value] as const,
|
||||
),
|
||||
queryFn: () =>
|
||||
client.launchermeta.manifest_v0.getManifest(apiLoaderName.value!, manifestFormatVersion.value),
|
||||
enabled: computed(() => !!apiLoaderName.value),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
})
|
||||
@@ -378,22 +386,101 @@ function getLoaderVersionsForGameVersion(
|
||||
const versionGroups = manifestQuery.data.value?.versionGroups
|
||||
if (!manifest) return []
|
||||
|
||||
const placeholder = manifest.find((x) => x.id === '${modrinth.gameVersion}')
|
||||
if (placeholder) return placeholder.loaders
|
||||
|
||||
const entry = manifest.find((x) => x.id === gameVersion)
|
||||
if (!entry) return []
|
||||
if (entry?.versionGroup) {
|
||||
return versionGroups?.find((group) => group.id === entry.versionGroup)?.loaders ?? []
|
||||
}
|
||||
|
||||
const placeholder = manifest.find((x) => x.id === loaderGameVersionPlaceholder)
|
||||
if (placeholder) return placeholder.loaders
|
||||
|
||||
return entry?.loaders ?? []
|
||||
}
|
||||
|
||||
function supportsMinecraftServer(version: Labrinth.Tags.v2.GameVersion): boolean {
|
||||
return Date.parse(version.date) >= minecraftServerDownloadsStartTime
|
||||
}
|
||||
|
||||
function getSupportedManifestGameVersions(): Set<string> | null {
|
||||
const manifest = manifestQuery.data.value?.gameVersions
|
||||
if (!manifest) return null
|
||||
|
||||
const hasPlaceholder = manifest.some((entry) => entry.id === loaderGameVersionPlaceholder)
|
||||
return new Set(
|
||||
manifest
|
||||
.filter((entry) => entry.id !== loaderGameVersionPlaceholder)
|
||||
.filter((entry) => hasPlaceholder || entry.loaders.length > 0 || !!entry.versionGroup)
|
||||
.map((entry) => entry.id),
|
||||
)
|
||||
}
|
||||
|
||||
function toApiLoader(loader: string): Archon.Content.v1.Modloader {
|
||||
if (loader === 'neoforge') return 'neo_forge'
|
||||
return loader as Archon.Content.v1.Modloader
|
||||
}
|
||||
|
||||
type InstallationCacheSnapshot = {
|
||||
server: Archon.Servers.v0.Server | undefined
|
||||
addons: Archon.Content.v1.Addons | undefined
|
||||
}
|
||||
|
||||
async function applyOptimisticInstallation(
|
||||
platform: string,
|
||||
gameVersion: string,
|
||||
loaderVersion: string | null,
|
||||
): Promise<InstallationCacheSnapshot> {
|
||||
await Promise.all([
|
||||
queryClient.cancelQueries({ queryKey: serverDetailQueryKey, exact: true }),
|
||||
queryClient.cancelQueries({ queryKey: addonsQueryKey, exact: true }),
|
||||
])
|
||||
|
||||
const snapshot = {
|
||||
server: queryClient.getQueryData<Archon.Servers.v0.Server>(serverDetailQueryKey),
|
||||
addons: queryClient.getQueryData<Archon.Content.v1.Addons>(addonsQueryKey),
|
||||
}
|
||||
const resolvedLoaderVersion = platform === 'vanilla' ? null : loaderVersion
|
||||
beginInstallation({
|
||||
type: 'platform',
|
||||
platform: platform as Extract<
|
||||
Archon.Websocket.v0.InstallProgressKey,
|
||||
{ type: 'platform' }
|
||||
>['platform'],
|
||||
platform_version: resolvedLoaderVersion ?? '',
|
||||
game_version: gameVersion,
|
||||
})
|
||||
|
||||
queryClient.setQueryData<Archon.Servers.v0.Server>(serverDetailQueryKey, (current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
status: 'installing',
|
||||
loader: formatLoaderLabel(platform) as Archon.Servers.v0.Loader,
|
||||
loader_version: resolvedLoaderVersion,
|
||||
mc_version: gameVersion,
|
||||
}
|
||||
: current,
|
||||
)
|
||||
queryClient.setQueryData<Archon.Content.v1.Addons>(addonsQueryKey, (current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
modloader: toApiLoader(platform),
|
||||
modloader_version: resolvedLoaderVersion,
|
||||
game_version: gameVersion,
|
||||
}
|
||||
: current,
|
||||
)
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
function rollbackOptimisticInstallation(snapshot: InstallationCacheSnapshot) {
|
||||
cancelOptimisticInstallation()
|
||||
queryClient.setQueryData(serverDetailQueryKey, snapshot.server)
|
||||
queryClient.setQueryData(addonsQueryKey, snapshot.addons)
|
||||
}
|
||||
|
||||
async function uploadLocalModpackWithSoftOverride() {
|
||||
const picked = await filePicker.pickModpackFile()
|
||||
if (!picked?.file) return false
|
||||
@@ -405,8 +492,11 @@ async function uploadLocalModpackWithSoftOverride() {
|
||||
{ softOverride: true },
|
||||
)
|
||||
await uploadProgressModal.value!.track(handle)
|
||||
beginInstallation({
|
||||
type: 'local_modpack',
|
||||
filename: picked.file.name,
|
||||
})
|
||||
emit('reinstall')
|
||||
await invalidateServerState()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -481,44 +571,37 @@ provideInstallationSettings({
|
||||
currentPlatform: computed(() => server.value?.loader?.toLowerCase() ?? 'vanilla'),
|
||||
currentGameVersion: computed(() => server.value?.mc_version ?? ''),
|
||||
currentLoaderVersion: computed(() => server.value?.loader_version ?? ''),
|
||||
requiresInstallation,
|
||||
availablePlatforms: ['vanilla', 'fabric', 'neoforge', 'forge', 'quilt', 'paper', 'purpur'],
|
||||
|
||||
editingPlatformRef: editingPlatform,
|
||||
editingGameVersionRef: editingGameVersion,
|
||||
|
||||
resolveGameVersions(loader, showSnapshots) {
|
||||
const serverVersions = tags.gameVersions.value.filter(supportsMinecraftServer)
|
||||
const versions = showSnapshots
|
||||
? tags.gameVersions.value
|
||||
: tags.gameVersions.value.filter((v) => v.version_type === 'release')
|
||||
? serverVersions
|
||||
: serverVersions.filter((v) => v.version_type === 'release')
|
||||
|
||||
if (loader && loader !== 'vanilla') {
|
||||
if (loader === 'paper') {
|
||||
const supported = paperSupportedVersionsQuery.data.value
|
||||
if (supported) {
|
||||
return versions
|
||||
.filter((v) => supported.has(v.version))
|
||||
.map((v) => ({ value: v.version, label: v.version }))
|
||||
}
|
||||
if (!supported) return []
|
||||
return versions
|
||||
.filter((v) => supported.has(v.version))
|
||||
.map((v) => ({ value: v.version, label: v.version }))
|
||||
} else if (loader === 'purpur') {
|
||||
const supported = purpurSupportedVersionsQuery.data.value
|
||||
if (supported) {
|
||||
return versions
|
||||
.filter((v) => supported.has(v.version))
|
||||
.map((v) => ({ value: v.version, label: v.version }))
|
||||
}
|
||||
if (!supported) return []
|
||||
return versions
|
||||
.filter((v) => supported.has(v.version))
|
||||
.map((v) => ({ value: v.version, label: v.version }))
|
||||
} else {
|
||||
const manifest = manifestQuery.data.value?.gameVersions
|
||||
if (manifest) {
|
||||
const hasPlaceholder = manifest.some((x) => x.id === '${modrinth.gameVersion}')
|
||||
if (!hasPlaceholder) {
|
||||
const supportedVersions = new Set(
|
||||
manifest.filter((x) => x.loaders.length > 0 || !!x.versionGroup).map((x) => x.id),
|
||||
)
|
||||
return versions
|
||||
.filter((v) => supportedVersions.has(v.version))
|
||||
.map((v) => ({ value: v.version, label: v.version }))
|
||||
}
|
||||
}
|
||||
const supportedVersions = getSupportedManifestGameVersions()
|
||||
if (!supportedVersions) return []
|
||||
return versions
|
||||
.filter((v) => supportedVersions.has(v.version))
|
||||
.map((v) => ({ value: v.version, label: v.version }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,33 +614,23 @@ provideInstallationSettings({
|
||||
},
|
||||
|
||||
resolveHasSnapshots(loader) {
|
||||
const serverVersions = tags.gameVersions.value.filter(supportsMinecraftServer)
|
||||
if (loader === 'vanilla') {
|
||||
return tags.gameVersions.value.some((v) => v.version_type !== 'release')
|
||||
return serverVersions.some((v) => v.version_type !== 'release')
|
||||
}
|
||||
if (loader === 'paper') {
|
||||
const supported = paperSupportedVersionsQuery.data.value
|
||||
if (!supported) return false
|
||||
return tags.gameVersions.value.some(
|
||||
(v) => v.version_type !== 'release' && supported.has(v.version),
|
||||
)
|
||||
return serverVersions.some((v) => v.version_type !== 'release' && supported.has(v.version))
|
||||
}
|
||||
if (loader === 'purpur') {
|
||||
const supported = purpurSupportedVersionsQuery.data.value
|
||||
if (!supported) return false
|
||||
return tags.gameVersions.value.some(
|
||||
(v) => v.version_type !== 'release' && supported.has(v.version),
|
||||
)
|
||||
return serverVersions.some((v) => v.version_type !== 'release' && supported.has(v.version))
|
||||
}
|
||||
const manifest = manifestQuery.data.value?.gameVersions
|
||||
if (!manifest) return false
|
||||
const hasPlaceholder = manifest.some((x) => x.id === '${modrinth.gameVersion}')
|
||||
if (hasPlaceholder) {
|
||||
return tags.gameVersions.value.some((v) => v.version_type !== 'release')
|
||||
}
|
||||
const supportedVersions = new Set(
|
||||
manifest.filter((x) => x.loaders.length > 0 || !!x.versionGroup).map((x) => x.id),
|
||||
)
|
||||
const supported = tags.gameVersions.value.filter((v) => supportedVersions.has(v.version))
|
||||
const supportedVersions = getSupportedManifestGameVersions()
|
||||
if (!supportedVersions) return false
|
||||
const supported = serverVersions.filter((v) => supportedVersions.has(v.version))
|
||||
return supported.some((v) => v.version_type !== 'release')
|
||||
},
|
||||
|
||||
@@ -569,6 +642,9 @@ provideInstallationSettings({
|
||||
const gameVersionChanged = gameVersion !== (server.value?.mc_version ?? '')
|
||||
const loaderVersionChanged =
|
||||
loaderVersionId !== null && loaderVersionId !== (server.value?.loader_version ?? '')
|
||||
const shouldInstallContent =
|
||||
requiresInstallation.value || platformChanged || loaderVersionChanged
|
||||
if (!shouldInstallContent && !gameVersionChanged) return
|
||||
|
||||
let resolvedLoaderVersion = loaderVersionId
|
||||
if (!resolvedLoaderVersion && platform !== 'vanilla') {
|
||||
@@ -576,32 +652,34 @@ provideInstallationSettings({
|
||||
resolvedLoaderVersion = versions[0]?.id ?? null
|
||||
}
|
||||
|
||||
const snapshot = await applyOptimisticInstallation(platform, gameVersion, resolvedLoaderVersion)
|
||||
debug('save: emitting reinstall before API call')
|
||||
emit(
|
||||
'reinstall',
|
||||
platformChanged || loaderVersionChanged
|
||||
shouldInstallContent
|
||||
? { loader: platform, lVersion: resolvedLoaderVersion, mVersion: gameVersion }
|
||||
: { mVersion: gameVersion },
|
||||
)
|
||||
try {
|
||||
if (platformChanged || loaderVersionChanged) {
|
||||
if (shouldInstallContent) {
|
||||
const request: Archon.Content.v1.InstallWorldContent = {
|
||||
content_variant: 'bare',
|
||||
loader: toApiLoader(platform),
|
||||
version: resolvedLoaderVersion ?? '',
|
||||
game_version: gameVersion || undefined,
|
||||
game_version: gameVersion,
|
||||
soft_override: true,
|
||||
}
|
||||
debug('save: platform/loader version changed, calling installContent', request)
|
||||
debug('save: calling installContent', request)
|
||||
await client.archon.content_v1.installContent(serverId, worldId.value!, request)
|
||||
} else if (gameVersionChanged) {
|
||||
debug('save: game version only, calling applyGameVersionUpdate', gameVersion)
|
||||
await client.archon.content_v1.applyGameVersionUpdate(serverId, worldId.value!, gameVersion)
|
||||
}
|
||||
debug('save: succeeded, invalidating')
|
||||
invalidateServerState()
|
||||
debug('save: succeeded')
|
||||
serverSettings.closeModal?.()
|
||||
} catch (err) {
|
||||
debug('save: failed, emitting reinstall-failed', err)
|
||||
rollbackOptimisticInstallation(snapshot)
|
||||
emit('reinstall-failed')
|
||||
addNotification({
|
||||
type: 'error',
|
||||
@@ -614,10 +692,10 @@ provideInstallationSettings({
|
||||
async repair() {
|
||||
if (setupActionDisabled.value) return
|
||||
debug('repair: called')
|
||||
beginInstallation({ type: 'unknown' })
|
||||
try {
|
||||
await client.archon.content_v1.repair(serverId, worldId.value!)
|
||||
debug('repair: API succeeded, invalidating')
|
||||
await invalidateServerState()
|
||||
debug('repair: API succeeded')
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: formatMessage(messages.repairStartedTitle),
|
||||
@@ -625,6 +703,7 @@ provideInstallationSettings({
|
||||
})
|
||||
} catch (err) {
|
||||
debug('repair: failed', err)
|
||||
cancelOptimisticInstallation()
|
||||
addNotification({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : formatMessage(messages.failedToRepair),
|
||||
@@ -656,6 +735,11 @@ provideInstallationSettings({
|
||||
modpack.value.spec.version_id,
|
||||
)
|
||||
debug('reinstallModpack: emitting reinstall before API call')
|
||||
beginInstallation({
|
||||
type: 'modrinth_modpack',
|
||||
project_id: modpack.value.spec.project_id,
|
||||
version_id: modpack.value.spec.version_id,
|
||||
})
|
||||
emit('reinstall')
|
||||
try {
|
||||
await client.archon.content_v1.installContent(serverId, worldId.value!, {
|
||||
@@ -667,10 +751,10 @@ provideInstallationSettings({
|
||||
},
|
||||
soft_override: true,
|
||||
})
|
||||
debug('reinstallModpack: installContent succeeded, invalidating')
|
||||
invalidateServerState()
|
||||
debug('reinstallModpack: installContent succeeded')
|
||||
} catch (err) {
|
||||
debug('reinstallModpack: failed, emitting reinstall-failed', err)
|
||||
cancelOptimisticInstallation()
|
||||
emit('reinstall-failed')
|
||||
addNotification({
|
||||
type: 'error',
|
||||
@@ -720,14 +804,7 @@ provideInstallationSettings({
|
||||
})
|
||||
} finally {
|
||||
debug('unlinkModpack: invalidating queries')
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['servers', 'detail', serverId],
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['content', 'list', 'v1', serverId],
|
||||
}),
|
||||
])
|
||||
await invalidateServerState()
|
||||
debug('unlinkModpack: invalidation complete')
|
||||
}
|
||||
},
|
||||
@@ -771,6 +848,11 @@ provideInstallationSettings({
|
||||
if (!modpackProjectId.value) return
|
||||
debug('onModpackVersionConfirm: called, version:', version.id)
|
||||
debug('onModpackVersionConfirm: emitting reinstall before API call')
|
||||
beginInstallation({
|
||||
type: 'modrinth_modpack',
|
||||
project_id: modpackProjectId.value,
|
||||
version_id: version.id,
|
||||
})
|
||||
emit('reinstall')
|
||||
try {
|
||||
await client.archon.content_v1.installContent(serverId, worldId.value!, {
|
||||
@@ -782,10 +864,10 @@ provideInstallationSettings({
|
||||
},
|
||||
soft_override: true,
|
||||
})
|
||||
debug('onModpackVersionConfirm: installContent succeeded, invalidating')
|
||||
invalidateServerState()
|
||||
debug('onModpackVersionConfirm: installContent succeeded')
|
||||
} catch (err) {
|
||||
debug('onModpackVersionConfirm: failed, emitting reinstall-failed', err)
|
||||
cancelOptimisticInstallation()
|
||||
emit('reinstall-failed')
|
||||
addNotification({
|
||||
type: 'error',
|
||||
@@ -867,6 +949,7 @@ provideInstallationSettings({
|
||||
const versions = getLoaderVersionsForGameVersion(platform, gameVersion)
|
||||
resolvedLoaderVersion = versions[0]?.id ?? null
|
||||
}
|
||||
const snapshot = await applyOptimisticInstallation(platform, gameVersion, resolvedLoaderVersion)
|
||||
emit('reinstall', { loader: platform, lVersion: resolvedLoaderVersion, mVersion: gameVersion })
|
||||
try {
|
||||
const request: Archon.Content.v1.InstallWorldContent = {
|
||||
@@ -878,10 +961,10 @@ provideInstallationSettings({
|
||||
}
|
||||
debug('saveWithoutAutoFix: calling installContent', request)
|
||||
await client.archon.content_v1.installContent(serverId, worldId.value!, request)
|
||||
debug('saveWithoutAutoFix: succeeded, invalidating')
|
||||
invalidateServerState()
|
||||
debug('saveWithoutAutoFix: succeeded')
|
||||
} catch (err) {
|
||||
debug('saveWithoutAutoFix: failed', err)
|
||||
rollbackOptimisticInstallation(snapshot)
|
||||
emit('reinstall-failed')
|
||||
addNotification({
|
||||
type: 'error',
|
||||
@@ -932,10 +1015,28 @@ watch(
|
||||
)
|
||||
|
||||
function onReinstall(event?: unknown) {
|
||||
if (resetServerDisabled.value) return
|
||||
if (resetServerDisabled.value && !installation.value) return
|
||||
installationSettingsLayout.value?.cancelEditing()
|
||||
modrinthServersConsole.clear()
|
||||
queryClient.removeQueries({ queryKey: ['servers', 'ws-state', serverId] })
|
||||
if (!installation.value) {
|
||||
const args = event as
|
||||
| { loader?: string; lVersion?: string; mVersion?: string | null }
|
||||
| undefined
|
||||
if (args?.loader && args.mVersion) {
|
||||
beginInstallation({
|
||||
type: 'platform',
|
||||
platform: args.loader as Extract<
|
||||
Archon.Websocket.v0.InstallProgressKey,
|
||||
{ type: 'platform' }
|
||||
>['platform'],
|
||||
platform_version: args.lVersion ?? '',
|
||||
game_version: args.mVersion,
|
||||
})
|
||||
} else {
|
||||
beginInstallation({ type: 'unknown' })
|
||||
}
|
||||
}
|
||||
emit('reinstall', event)
|
||||
serverSettings.closeModal?.()
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
import { type Archon, type Labrinth, ModrinthApiError } from '@modrinth/api-client'
|
||||
import { ClipboardCopyIcon } from '@modrinth/assets'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useIntervalFn } from '@vueuse/core'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ReadyTransition from '#ui/components/base/ReadyTransition.vue'
|
||||
import UnknownFileWarningModal from '#ui/components/modal/UnknownFileWarningModal.vue'
|
||||
import { useUploadSessionUpload } from '#ui/composables/hosting/kyros-session-upload'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { waitForServerContextRuntimeReady } from '#ui/composables/server-context-runtime'
|
||||
import { useServerPermissions } from '#ui/composables/server-permissions'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
@@ -18,13 +18,6 @@ import {
|
||||
injectServerSettingsModal,
|
||||
} from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
import {
|
||||
type PendingServerContentInstall,
|
||||
pendingServerContentInstallsEvent,
|
||||
readPendingServerContentInstallBaseline,
|
||||
readPendingServerContentInstalls,
|
||||
removePendingServerContentInstall,
|
||||
} from '#ui/utils/server-content-installing'
|
||||
import { versionChangesGameVersion } from '#ui/utils/version-compatibility'
|
||||
|
||||
import type { BrowseInstallPlan } from '../../../shared/browse-tab/composables/install-logic'
|
||||
@@ -117,7 +110,7 @@ const messages = defineMessages({
|
||||
})
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { server, worldId, busyReasons, isSyncingContent, uploadState, cancelUpload } =
|
||||
const { server, worldId, busyReasons, installProgressItems, uploadState, cancelUpload } =
|
||||
injectModrinthServerContext()
|
||||
const contentUploadSession = useUploadSessionUpload({
|
||||
client,
|
||||
@@ -176,7 +169,6 @@ const setupActionDisabled = computed(() => !canSetup.value || busyReasons.value.
|
||||
const isInstallingContent = computed(
|
||||
() =>
|
||||
server.value?.status === 'installing' ||
|
||||
isSyncingContent.value ||
|
||||
busyReasons.value.some(
|
||||
(r) =>
|
||||
r.reason.id === 'servers.busy.installing' || r.reason.id === 'servers.busy.syncing-content',
|
||||
@@ -201,6 +193,15 @@ const setupActionBusyMessage = computed(() => {
|
||||
return filteredReasons.length > 0 ? formatMessage(filteredReasons[0].reason) : null
|
||||
})
|
||||
|
||||
const currentWorldInstallProgressItems = computed(() =>
|
||||
installProgressItems.value.filter((item) => item.world_id === worldId.value),
|
||||
)
|
||||
const contentActionDisabled = computed(() => !canSetup.value || busyReasons.value.length > 0)
|
||||
const contentActionBusyMessage = computed(() => {
|
||||
if (!canSetup.value) return permissionDeniedMessage.value
|
||||
return busyReasons.value.length > 0 ? formatMessage(busyReasons.value[0].reason) : null
|
||||
})
|
||||
|
||||
const modpackProjectId = computed(() => {
|
||||
const spec = contentQuery.data.value?.modpack?.spec
|
||||
return spec?.platform === 'modrinth' ? spec.project_id : null
|
||||
@@ -291,6 +292,8 @@ const managedContent = computed<ManagedContentData | null>(() => {
|
||||
: undefined,
|
||||
updatedAt: isLocal ? undefined : (mp.date_published ?? undefined),
|
||||
},
|
||||
disabled: setupActionDisabled.value,
|
||||
disabledText: setupActionBusyMessage.value ?? formatMessage(commonMessages.installingLabel),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -315,12 +318,10 @@ const addonLookup = computed(() => {
|
||||
return map
|
||||
})
|
||||
|
||||
const pendingServerContentInstalls = ref<PendingServerContentInstall[]>([])
|
||||
const projectMetadataBatchSize = 800
|
||||
const contentProjectIds = computed(() =>
|
||||
[...(contentQuery.data.value?.addons ?? []), ...modpackAddons.value]
|
||||
.map((addon) => addon.project_id)
|
||||
.concat(pendingServerContentInstalls.value.map((item) => item.projectId))
|
||||
.filter((id): id is string => !!id)
|
||||
.filter((id, index, ids) => ids.indexOf(id) === index)
|
||||
.sort(),
|
||||
@@ -341,43 +342,64 @@ const contentProjectsQuery = useQuery({
|
||||
const contentProjectsById = computed(
|
||||
() => new Map((contentProjectsQuery.data.value ?? []).map((project) => [project.id, project])),
|
||||
)
|
||||
const lastStableContentKeys = ref<Set<string>>(new Set())
|
||||
const contentInstallBaselineKeys = ref<Set<string> | null>(null)
|
||||
const contentInstallAddedKeys = ref<Set<string>>(new Set())
|
||||
const isFlushingStoredServerInstalls = ref(false)
|
||||
const { pause: pausePendingInstallPoll, resume: resumePendingInstallPoll } = useIntervalFn(
|
||||
() => {
|
||||
if (pendingServerContentInstalls.value.length === 0 || contentQuery.isFetching.value) return
|
||||
void contentQuery.refetch()
|
||||
},
|
||||
5000,
|
||||
{ immediate: false },
|
||||
|
||||
function normalizeInstallFilename(filename: string) {
|
||||
const normalized = filename.endsWith('.disabled')
|
||||
? filename.slice(0, -'.disabled'.length)
|
||||
: filename
|
||||
return normalized.toLowerCase()
|
||||
}
|
||||
|
||||
type FileInstallProgressItem = Archon.Websocket.v0.InstallProgressItem & {
|
||||
key: Archon.Websocket.v0.InstallProgressFileKey
|
||||
}
|
||||
|
||||
const fileInstallProgressItems = computed<FileInstallProgressItem[]>(() =>
|
||||
currentWorldInstallProgressItems.value.filter(
|
||||
(item): item is FileInstallProgressItem => item.key.type === 'file',
|
||||
),
|
||||
)
|
||||
|
||||
function syncPendingServerContentInstalls() {
|
||||
pendingServerContentInstalls.value = readPendingServerContentInstalls(serverId, worldId.value)
|
||||
function getFileInstallFilenames(key: Archon.Websocket.v0.InstallProgressFileKey) {
|
||||
return [key.source_filename, key.target_filename]
|
||||
.filter((filename): filename is string => !!filename)
|
||||
.map(normalizeInstallFilename)
|
||||
}
|
||||
|
||||
function handlePendingServerContentInstallsChanged(event: Event) {
|
||||
const detail = (event as CustomEvent<{ serverId?: string | null; worldId?: string | null }>)
|
||||
.detail
|
||||
if (detail?.serverId !== serverId || detail?.worldId !== worldId.value) return
|
||||
syncPendingServerContentInstalls()
|
||||
void flushStoredServerInstalls()
|
||||
function isFileInstallActive(item: FileInstallProgressItem) {
|
||||
return item.error == null && item.progress !== 100
|
||||
}
|
||||
|
||||
function getAddonInstallKey(addon: Archon.Content.v1.Addon) {
|
||||
return addon.project_id ?? addon.filename
|
||||
function getContentItemInstallFilename(item: ContentItem) {
|
||||
const filename = item.version?.file_name || item.file_name
|
||||
return normalizeInstallFilename(filename)
|
||||
}
|
||||
|
||||
function getAddonInstallKeys(addons: Archon.Content.v1.Addon[]) {
|
||||
const keys = new Set<string>()
|
||||
for (const addon of addons) {
|
||||
keys.add(getAddonInstallKey(addon))
|
||||
function getContentItemInstallProgress(item: ContentItem): FileInstallProgressItem | undefined {
|
||||
const projectId = item.project?.id
|
||||
const versionId = item.version?.id
|
||||
const filename = getContentItemInstallFilename(item)
|
||||
|
||||
return fileInstallProgressItems.value.find((progressItem) => {
|
||||
const key = progressItem.key
|
||||
if (key.project_id === projectId) return true
|
||||
if (key.version_id === versionId) return true
|
||||
return getFileInstallFilenames(key).includes(filename)
|
||||
})
|
||||
}
|
||||
|
||||
function decorateContentItemWithInstallProgress(
|
||||
contentItem: ContentItem,
|
||||
installProgress: FileInstallProgressItem,
|
||||
): ContentItem {
|
||||
return {
|
||||
...contentItem,
|
||||
installProgress: isFileInstallActive(installProgress) ? installProgress.progress : undefined,
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
const isFlushingStoredServerInstalls = ref(false)
|
||||
|
||||
function getInstalledProjectIds() {
|
||||
return new Set(
|
||||
(contentQuery.data.value?.addons ?? [])
|
||||
@@ -424,53 +446,6 @@ async function resolveStoredServerAddonPlans(plans: BrowseInstallPlan[]) {
|
||||
})
|
||||
}
|
||||
|
||||
function addonMatchesPendingInstall(
|
||||
addon: Archon.Content.v1.Addon,
|
||||
pendingInstall: PendingServerContentInstall,
|
||||
) {
|
||||
return (
|
||||
addon.project_id === pendingInstall.projectId ||
|
||||
addon.version?.id === pendingInstall.versionId ||
|
||||
(!!pendingInstall.fileName && addon.filename === pendingInstall.fileName)
|
||||
)
|
||||
}
|
||||
|
||||
function removeResolvedPendingServerContentInstalls(addons: Archon.Content.v1.Addon[]) {
|
||||
if (addons.length === 0 || pendingServerContentInstalls.value.length === 0) return
|
||||
|
||||
for (const pendingInstall of pendingServerContentInstalls.value) {
|
||||
if (addons.some((addon) => addonMatchesPendingInstall(addon, pendingInstall))) {
|
||||
removePendingServerContentInstall(serverId, worldId.value, pendingInstall.projectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function syncContentInstallKeys(
|
||||
addons: Archon.Content.v1.Addon[] = contentQuery.data.value?.addons ?? [],
|
||||
) {
|
||||
const currentKeys = getAddonInstallKeys(addons)
|
||||
if (isSyncingContent.value) {
|
||||
if (!contentInstallBaselineKeys.value) {
|
||||
contentInstallBaselineKeys.value =
|
||||
readPendingServerContentInstallBaseline(serverId, worldId.value) ??
|
||||
new Set(lastStableContentKeys.value)
|
||||
}
|
||||
|
||||
const nextAddedKeys = new Set(contentInstallAddedKeys.value)
|
||||
for (const key of currentKeys) {
|
||||
if (!contentInstallBaselineKeys.value.has(key)) {
|
||||
nextAddedKeys.add(key)
|
||||
}
|
||||
}
|
||||
contentInstallAddedKeys.value = nextAddedKeys
|
||||
return
|
||||
}
|
||||
|
||||
lastStableContentKeys.value = currentKeys
|
||||
contentInstallBaselineKeys.value = null
|
||||
contentInstallAddedKeys.value = new Set()
|
||||
}
|
||||
|
||||
async function flushStoredServerInstalls() {
|
||||
const wid = worldId.value
|
||||
if (!wid || isFlushingStoredServerInstalls.value) return
|
||||
@@ -478,6 +453,17 @@ async function flushStoredServerInstalls() {
|
||||
const queuedPlans = getStoredServerAddonInstallQueue(serverId, wid)
|
||||
if (queuedPlans.size === 0) return
|
||||
|
||||
try {
|
||||
await waitForServerContextRuntimeReady(client, serverId)
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(messages.failedToInstallContent),
|
||||
text: error instanceof Error ? error.message : undefined,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
isFlushingStoredServerInstalls.value = true
|
||||
try {
|
||||
const result = await flushStoredServerAddonInstallQueue({
|
||||
@@ -492,9 +478,6 @@ async function flushStoredServerInstalls() {
|
||||
})
|
||||
|
||||
if (!result.ok) {
|
||||
for (const plan of result.attemptedPlans) {
|
||||
removePendingServerContentInstall(serverId, wid, plan.projectId)
|
||||
}
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(messages.failedToInstallContent),
|
||||
@@ -508,224 +491,39 @@ async function flushStoredServerInstalls() {
|
||||
}
|
||||
} finally {
|
||||
isFlushingStoredServerInstalls.value = false
|
||||
syncPendingServerContentInstalls()
|
||||
}
|
||||
}
|
||||
|
||||
function pendingInstallToContentItem(item: PendingServerContentInstall): ContentItem {
|
||||
const projectMetadata = contentProjectsById.value.get(item.projectId)
|
||||
return {
|
||||
project: {
|
||||
...(projectMetadata ?? {}),
|
||||
id: item.projectId,
|
||||
slug: item.slug ?? projectMetadata?.slug ?? item.projectId,
|
||||
title: projectMetadata?.title ?? item.title,
|
||||
icon_url: item.iconUrl ?? projectMetadata?.icon_url ?? undefined,
|
||||
},
|
||||
version: {
|
||||
id: item.versionId,
|
||||
version_number:
|
||||
item.versionName ?? item.versionNumber ?? formatMessage(commonMessages.installingLabel),
|
||||
file_name: item.fileName ?? formatMessage(commonMessages.installingLabel),
|
||||
},
|
||||
owner: item.owner
|
||||
? {
|
||||
id: item.owner.id,
|
||||
name: item.owner.name,
|
||||
type: item.owner.type,
|
||||
avatar_url: getContentOwnerAvatarUrl(item.owner),
|
||||
link: item.owner.link,
|
||||
}
|
||||
: undefined,
|
||||
id: `installing:${item.projectId}`,
|
||||
enabled: true,
|
||||
file_name: `installing:${item.projectId}`,
|
||||
project_type: item.contentType,
|
||||
has_update: false,
|
||||
update_version_id: null,
|
||||
installing: true,
|
||||
}
|
||||
}
|
||||
|
||||
const rawContentItems = computed<ContentItem[]>(() => {
|
||||
const addons = contentQuery.data.value?.addons ?? []
|
||||
const pendingProjectIds = new Set(
|
||||
pendingServerContentInstalls.value.map((item) => item.projectId),
|
||||
)
|
||||
const pendingInstallByProjectId = new Map(
|
||||
pendingServerContentInstalls.value.map((item) => [item.projectId, item]),
|
||||
)
|
||||
const pendingInstallByVersionId = new Map(
|
||||
pendingServerContentInstalls.value.map((item) => [item.versionId, item]),
|
||||
)
|
||||
const pendingInstallByFileName = new Map<string, PendingServerContentInstall>()
|
||||
for (const item of pendingServerContentInstalls.value) {
|
||||
if (item.fileName) {
|
||||
pendingInstallByFileName.set(item.fileName, item)
|
||||
}
|
||||
}
|
||||
const installingContentKeys = new Set([...pendingProjectIds, ...contentInstallAddedKeys.value])
|
||||
const resolvedPendingProjectIds = new Set(
|
||||
pendingServerContentInstalls.value
|
||||
.filter((item) => addons.some((addon) => addonMatchesPendingInstall(addon, item)))
|
||||
.map((item) => item.projectId),
|
||||
)
|
||||
const pendingItems = pendingServerContentInstalls.value
|
||||
.filter((item) => !resolvedPendingProjectIds.has(item.projectId))
|
||||
.map(pendingInstallToContentItem)
|
||||
const addonItems = addons.map((addon) => {
|
||||
const contentItems = computed<ContentItem[]>(() =>
|
||||
(contentQuery.data.value?.addons ?? []).map((addon) => {
|
||||
const contentItem = addonToContentItem(addon)
|
||||
const pendingItem =
|
||||
(addon.project_id ? pendingInstallByProjectId.get(addon.project_id) : null) ??
|
||||
(addon.version?.id ? pendingInstallByVersionId.get(addon.version.id) : null) ??
|
||||
pendingInstallByFileName.get(addon.filename) ??
|
||||
null
|
||||
const installing = !!pendingItem || installingContentKeys.has(getAddonInstallKey(addon))
|
||||
if (!contentItem.installing) return contentItem
|
||||
|
||||
if (!installing || !pendingItem) {
|
||||
return {
|
||||
...contentItem,
|
||||
installing,
|
||||
}
|
||||
}
|
||||
|
||||
const pendingContentItem = pendingInstallToContentItem(pendingItem)
|
||||
return {
|
||||
...contentItem,
|
||||
project: {
|
||||
...contentItem.project,
|
||||
slug: pendingContentItem.project.slug,
|
||||
title: pendingContentItem.project.title,
|
||||
icon_url: contentItem.project.icon_url ?? pendingContentItem.project.icon_url,
|
||||
},
|
||||
version: {
|
||||
id: pendingContentItem.version?.id ?? contentItem.version?.id ?? contentItem.file_name,
|
||||
version_number:
|
||||
pendingContentItem.version?.version_number ??
|
||||
contentItem.version?.version_number ??
|
||||
formatMessage(commonMessages.installingLabel),
|
||||
file_name:
|
||||
pendingContentItem.version?.file_name ??
|
||||
contentItem.version?.file_name ??
|
||||
contentItem.file_name,
|
||||
},
|
||||
owner: pendingContentItem.owner ?? contentItem.owner,
|
||||
installing,
|
||||
}
|
||||
})
|
||||
|
||||
return [...addonItems, ...pendingItems]
|
||||
})
|
||||
|
||||
const displayedContentItems = ref<ContentItem[]>([])
|
||||
const contentItems = computed<ContentItem[]>(() => displayedContentItems.value)
|
||||
const installProgress = getContentItemInstallProgress(contentItem)
|
||||
return installProgress
|
||||
? decorateContentItemWithInstallProgress(contentItem, installProgress)
|
||||
: contentItem
|
||||
}),
|
||||
)
|
||||
const contentReadyPending = computed(
|
||||
() =>
|
||||
contentQuery.isLoading.value &&
|
||||
contentQuery.data.value === undefined &&
|
||||
pendingServerContentInstalls.value.length === 0 &&
|
||||
displayedContentItems.value.length === 0,
|
||||
contentItems.value.length === 0,
|
||||
)
|
||||
|
||||
function getContentItemDisplayKey(item: ContentItem) {
|
||||
return item.project?.id ?? item.file_name ?? item.id
|
||||
}
|
||||
|
||||
function getContentItemId(item: ContentItem) {
|
||||
return item.file_name ?? item.id
|
||||
}
|
||||
|
||||
function mergeFragileContentItems(items: ContentItem[]) {
|
||||
const nextItems = new Map(items.map((item) => [getContentItemDisplayKey(item), item]))
|
||||
const mergedItems = displayedContentItems.value.map((item) => {
|
||||
const key = getContentItemDisplayKey(item)
|
||||
const nextItem = nextItems.get(key)
|
||||
if (!nextItem) return item
|
||||
|
||||
nextItems.delete(key)
|
||||
return nextItem
|
||||
})
|
||||
|
||||
return [...mergedItems, ...nextItems.values()]
|
||||
}
|
||||
|
||||
watch(
|
||||
[
|
||||
rawContentItems,
|
||||
isSyncingContent,
|
||||
() => contentQuery.isFetching.value,
|
||||
() => contentQuery.isLoading.value,
|
||||
],
|
||||
([items, syncing, isFetching, isLoading]) => {
|
||||
if (syncing) {
|
||||
if (items.length > 0) {
|
||||
displayedContentItems.value = mergeFragileContentItems(items)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (items.length > 0 || (!isFetching && !isLoading)) {
|
||||
displayedContentItems.value = items
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
[isSyncingContent, () => contentQuery.data.value?.addons],
|
||||
([, addons]) => {
|
||||
syncContentInstallKeys(addons ?? [])
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
[() => contentQuery.data.value?.addons, pendingServerContentInstalls],
|
||||
([addons]) => {
|
||||
removeResolvedPendingServerContentInstalls(addons ?? [])
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => pendingServerContentInstalls.value.length > 0,
|
||||
(hasPendingInstalls) => {
|
||||
if (hasPendingInstalls) {
|
||||
resumePendingInstallPoll()
|
||||
} else {
|
||||
pausePendingInstallPoll()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
worldId,
|
||||
() => {
|
||||
syncPendingServerContentInstalls()
|
||||
syncContentInstallKeys()
|
||||
void flushStoredServerInstalls()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
syncPendingServerContentInstalls()
|
||||
void flushStoredServerInstalls()
|
||||
window.addEventListener(
|
||||
pendingServerContentInstallsEvent,
|
||||
handlePendingServerContentInstallsChanged,
|
||||
)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
pausePendingInstallPoll()
|
||||
window.removeEventListener(
|
||||
pendingServerContentInstallsEvent,
|
||||
handlePendingServerContentInstallsChanged,
|
||||
)
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: ({ addon }: { addon: Archon.Content.v1.Addon }) =>
|
||||
client.archon.content_v1.deleteAddon(serverId, worldId.value!, {
|
||||
@@ -793,14 +591,14 @@ const toggleMutation = useMutation({
|
||||
})
|
||||
|
||||
async function handleToggleEnabled(item: ContentItem) {
|
||||
if (setupActionDisabled.value) return
|
||||
if (contentActionDisabled.value) return
|
||||
const addon = addonLookup.value.get(item.file_name)
|
||||
if (!addon) return
|
||||
await toggleMutation.mutateAsync({ addon })
|
||||
}
|
||||
|
||||
async function handleDeleteItem(item: ContentItem) {
|
||||
if (setupActionDisabled.value) return
|
||||
if (contentActionDisabled.value) return
|
||||
const addon = addonLookup.value.get(item.file_name)
|
||||
if (!addon) return
|
||||
await deleteMutation.mutateAsync({ addon })
|
||||
@@ -808,6 +606,7 @@ async function handleDeleteItem(item: ContentItem) {
|
||||
|
||||
function itemsToAddonRequests(items: ContentItem[]): Archon.Content.v1.RemoveAddonRequest[] {
|
||||
return items.flatMap((item) => {
|
||||
if (item.installing) return []
|
||||
const addon = addonLookup.value.get(item.file_name)
|
||||
if (!addon) return []
|
||||
return [{ filename: addon.filename, kind: addon.kind }]
|
||||
@@ -815,7 +614,7 @@ function itemsToAddonRequests(items: ContentItem[]): Archon.Content.v1.RemoveAdd
|
||||
}
|
||||
|
||||
async function handleBulkDelete(items: ContentItem[]) {
|
||||
if (setupActionDisabled.value) return
|
||||
if (contentActionDisabled.value) return
|
||||
const requests = itemsToAddonRequests(items)
|
||||
if (requests.length === 0) return
|
||||
try {
|
||||
@@ -831,7 +630,7 @@ async function handleBulkDelete(items: ContentItem[]) {
|
||||
}
|
||||
|
||||
async function handleBulkEnable(items: ContentItem[]) {
|
||||
if (setupActionDisabled.value) return
|
||||
if (contentActionDisabled.value) return
|
||||
const requests = itemsToAddonRequests(items)
|
||||
if (requests.length === 0) return
|
||||
try {
|
||||
@@ -847,7 +646,7 @@ async function handleBulkEnable(items: ContentItem[]) {
|
||||
}
|
||||
|
||||
async function handleBulkDisable(items: ContentItem[]) {
|
||||
if (setupActionDisabled.value) return
|
||||
if (contentActionDisabled.value) return
|
||||
const requests = itemsToAddonRequests(items)
|
||||
if (requests.length === 0) return
|
||||
try {
|
||||
@@ -916,7 +715,7 @@ const currentLoader = computed(
|
||||
)
|
||||
|
||||
function handleBrowseContent() {
|
||||
if (setupActionDisabled.value) return
|
||||
if (contentActionDisabled.value) return
|
||||
const contentType = type.value
|
||||
if (browseServerContent && ['mod', 'plugin', 'datapack'].includes(contentType)) {
|
||||
browseServerContent({
|
||||
@@ -934,7 +733,7 @@ function handleBrowseContent() {
|
||||
}
|
||||
|
||||
function handleUploadFiles() {
|
||||
if (setupActionDisabled.value) return
|
||||
if (contentActionDisabled.value) return
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.multiple = true
|
||||
@@ -1039,13 +838,14 @@ function addonToContentItem(addon: AddonWithUiState): ContentItem {
|
||||
id: addon.id ?? addon.filename,
|
||||
enabled: !addon.disabled,
|
||||
file_name: addon.filename,
|
||||
date_added: addon.btime,
|
||||
project_type: addon.kind,
|
||||
has_update: !!addon.has_update,
|
||||
update_version_id: addon.has_update,
|
||||
environment: addon.version?.environment ?? undefined,
|
||||
pack_client_retained: addon.pack_client_retained,
|
||||
pack_client_depends: addon.pack_client_depends,
|
||||
installing: addon.installing,
|
||||
installing: addon.installing ?? addon.status === 'pending',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1078,7 +878,7 @@ async function handleViewModpackContent() {
|
||||
}
|
||||
|
||||
async function handleModpackContentToggle(item: ContentItem) {
|
||||
if (setupActionDisabled.value) return
|
||||
if (contentActionDisabled.value) return
|
||||
const addon = addonLookup.value.get(item.file_name)
|
||||
if (!addon) return
|
||||
modpackContentModal.value?.updateItem(item.file_name, { disabled: true })
|
||||
@@ -1109,7 +909,7 @@ async function handleModpackContentToggle(item: ContentItem) {
|
||||
}
|
||||
|
||||
async function handleModpackBulkToggle(items: ContentItem[], enable: boolean) {
|
||||
if (setupActionDisabled.value) return
|
||||
if (contentActionDisabled.value) return
|
||||
const requests = itemsToAddonRequests(items)
|
||||
if (requests.length === 0) return
|
||||
|
||||
@@ -1176,9 +976,9 @@ async function handleModpackUnlinkConfirm() {
|
||||
}
|
||||
|
||||
async function handleBulkUpdate(items: ContentItem[]) {
|
||||
if (setupActionDisabled.value) return
|
||||
if (contentActionDisabled.value) return
|
||||
const addons = items
|
||||
.filter((item) => item.has_update)
|
||||
.filter((item) => item.has_update && !item.installing)
|
||||
.map((item) => ({
|
||||
filename: item.file_name,
|
||||
version_id: item.update_version_id ?? undefined,
|
||||
@@ -1222,6 +1022,7 @@ async function handleSwitchVersion(item: ContentItem) {
|
||||
}
|
||||
|
||||
async function handleModpackUpdate() {
|
||||
if (setupActionDisabled.value) return
|
||||
const mp = contentQuery.data.value?.modpack
|
||||
if (!mp || mp.spec.platform !== 'modrinth') return
|
||||
|
||||
@@ -1276,8 +1077,8 @@ function resetUpdateState() {
|
||||
}
|
||||
|
||||
function handleModalUpdate(selectedVersion: Labrinth.Versions.v2.Version, event?: MouseEvent) {
|
||||
if (setupActionDisabled.value) return
|
||||
if (updatingModpack.value) {
|
||||
if (setupActionDisabled.value) return
|
||||
pendingModpackUpdateVersion.value = selectedVersion
|
||||
|
||||
const mpSpec = contentQuery.data.value?.modpack?.spec
|
||||
@@ -1298,6 +1099,7 @@ function handleModalUpdate(selectedVersion: Labrinth.Versions.v2.Version, event?
|
||||
return
|
||||
}
|
||||
|
||||
if (contentActionDisabled.value) return
|
||||
performUpdate(selectedVersion)
|
||||
}
|
||||
|
||||
@@ -1314,7 +1116,11 @@ function setAddonInstalling(filename: string, installing: boolean) {
|
||||
}
|
||||
|
||||
async function performUpdate(selectedVersion: Labrinth.Versions.v2.Version) {
|
||||
if (setupActionDisabled.value) return
|
||||
if (
|
||||
(updatingModpack.value && setupActionDisabled.value) ||
|
||||
(!updatingModpack.value && contentActionDisabled.value)
|
||||
)
|
||||
return
|
||||
const item = updatingProject.value
|
||||
if (item) {
|
||||
setAddonInstalling(item.file_name, true)
|
||||
@@ -1393,8 +1199,8 @@ provideContentManager({
|
||||
error: computed(() => contentQuery.error.value ?? null),
|
||||
managedContent,
|
||||
isPackLocked: ref(false),
|
||||
isBusy: setupActionDisabled,
|
||||
busyMessage: setupActionBusyMessage,
|
||||
isBusy: contentActionDisabled,
|
||||
busyMessage: contentActionBusyMessage,
|
||||
disableAddContent: computed(() => !canSetup.value),
|
||||
disableAddContentTooltip: permissionDeniedMessage.value,
|
||||
contentTypeLabel: type,
|
||||
@@ -1470,8 +1276,8 @@ provideContentManager({
|
||||
:header="formatMessage(messages.modpackContent)"
|
||||
enable-toggle
|
||||
show-environment-warnings
|
||||
:action-disabled="setupActionDisabled"
|
||||
:action-disabled-tooltip="setupActionBusyMessage ?? undefined"
|
||||
:action-disabled="contentActionDisabled"
|
||||
:action-disabled-tooltip="contentActionBusyMessage ?? undefined"
|
||||
@update:enabled="handleModpackContentToggle"
|
||||
@bulk:enable="handleModpackBulkToggle($event, true)"
|
||||
@bulk:disable="handleModpackBulkToggle($event, false)"
|
||||
@@ -1504,8 +1310,10 @@ provideContentManager({
|
||||
"
|
||||
:loading="loadingVersions"
|
||||
:loading-changelog="loadingChangelog"
|
||||
:action-disabled="setupActionDisabled"
|
||||
:action-disabled-tooltip="setupActionBusyMessage ?? undefined"
|
||||
:action-disabled="updatingModpack ? setupActionDisabled : contentActionDisabled"
|
||||
:action-disabled-tooltip="
|
||||
(updatingModpack ? setupActionBusyMessage : contentActionBusyMessage) ?? undefined
|
||||
"
|
||||
@update="handleModalUpdate"
|
||||
@cancel="resetUpdateState"
|
||||
@version-select="handleVersionSelect"
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
|
||||
<template #actions>
|
||||
<PageHeaderActions>
|
||||
<PanelServerActionButton :disabled="!!installError" />
|
||||
<PanelServerActionButton />
|
||||
<Tooltip
|
||||
theme="dismissable-prompt"
|
||||
:triggers="[]"
|
||||
@@ -217,7 +217,6 @@
|
||||
size="xl"
|
||||
label="More server options"
|
||||
:options="serverMenuOptions"
|
||||
:disabled="!!installError"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
</TeleportOverflowMenu>
|
||||
@@ -244,92 +243,6 @@
|
||||
:class="containedLayout ? 'flex min-h-0 flex-col overflow-hidden' : 'h-full'"
|
||||
:style="{ '--si': 2 }"
|
||||
>
|
||||
<div
|
||||
v-if="installError"
|
||||
class="mx-auto mb-4 flex justify-between gap-2 rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
|
||||
>
|
||||
<div class="flex flex-row gap-4">
|
||||
<IssuesIcon class="hidden h-8 w-8 shrink-0 text-red sm:block" />
|
||||
<div class="flex flex-col gap-2 leading-[150%]">
|
||||
<div class="flex items-center gap-3">
|
||||
<IssuesIcon class="flex h-8 w-8 shrink-0 text-red sm:hidden" />
|
||||
<div class="flex gap-2 text-2xl font-bold">{{ errorTitle }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="errorTitle.toLocaleLowerCase() === 'installation error'"
|
||||
class="font-normal"
|
||||
>
|
||||
<div
|
||||
v-if="
|
||||
errorMessage.toLocaleLowerCase() === 'the specified version may be incorrect'
|
||||
"
|
||||
>
|
||||
An invalid loader or Minecraft version was specified and could not be installed.
|
||||
<ul class="m-0 mt-4 p-0 pl-4">
|
||||
<li>
|
||||
If this version of Minecraft was released recently, please check if Modrinth
|
||||
Hosting supports it.
|
||||
</li>
|
||||
<li>
|
||||
If you've installed a modpack, it may have been packaged incorrectly or may
|
||||
not be compatible with the loader.
|
||||
</li>
|
||||
<li>
|
||||
Your server may need to be reinstalled with a valid mod loader and version.
|
||||
You can change the loader by clicking the "Change Loader" button.
|
||||
</li>
|
||||
<li>
|
||||
If you're stuck, please contact Modrinth Support with the information below:
|
||||
</li>
|
||||
</ul>
|
||||
<Button class="mt-2" @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="errorMessage.toLocaleLowerCase() === 'internal error'">
|
||||
An internal error occurred while installing your server. Don't fret — try
|
||||
reinstalling your server, and if the problem persists, please contact Modrinth
|
||||
support with your server's debug information.
|
||||
</div>
|
||||
<div
|
||||
v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'"
|
||||
>
|
||||
An error occurred while installing your server because Modrinth Hosting does not
|
||||
support the version of Minecraft or the loader you specified. Try reinstalling
|
||||
your server with a different version or loader, and if the problem persists,
|
||||
please contact Modrinth Support with your server's debug information.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="errorTitle === 'Installation error'"
|
||||
class="mt-2 flex flex-col gap-4 sm:flex-row"
|
||||
>
|
||||
<Button v-if="errorLog" @click="openInstallLog"
|
||||
><FileIcon />Open Installation Log</Button
|
||||
>
|
||||
<Button @click="copyServerDebugInfo">
|
||||
<CopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
Copy Debug Info
|
||||
</Button>
|
||||
<Button
|
||||
type="colored"
|
||||
color="red"
|
||||
class="whitespace-pre"
|
||||
@click="openServerSettingsModal('installation')"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
Change Loader
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="serverData.is_medal" class="mb-4">
|
||||
<MedalServerCountdown
|
||||
:server-id="serverId"
|
||||
@@ -359,9 +272,7 @@
|
||||
|
||||
<ServerPanelAdmonitions
|
||||
class="mb-4 shrink-0"
|
||||
:sync-progress="syncProgress"
|
||||
:content-error="contentError"
|
||||
@content-retry="handleContentRetry"
|
||||
@installation-retry="handleInstallationRetry"
|
||||
/>
|
||||
<slot :on-reinstall="onReinstall" :on-reinstall-failed="onReinstallFailed" />
|
||||
</div>
|
||||
@@ -394,13 +305,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import { getNodeWebSocketUrl, ModrinthApiError, NuxtModrinthClient } from '@modrinth/api-client'
|
||||
import { ModrinthApiError, NuxtModrinthClient } from '@modrinth/api-client'
|
||||
import {
|
||||
BoxesIcon,
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
DatabaseBackupIcon,
|
||||
FileIcon,
|
||||
FolderOpenIcon,
|
||||
IssuesIcon,
|
||||
LayoutTemplateIcon,
|
||||
@@ -408,7 +317,6 @@ import {
|
||||
LoaderCircleIcon,
|
||||
LockIcon,
|
||||
MoreVerticalIcon,
|
||||
RightArrowIcon,
|
||||
ServerIcon as ServerAssetIcon,
|
||||
SettingsIcon,
|
||||
TimerIcon,
|
||||
@@ -418,14 +326,14 @@ import {
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useStorage, useTimeoutFn } from '@vueuse/core'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { Tooltip } from 'floating-vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import Avatar from '#ui/components/base/Avatar.vue'
|
||||
import { Button, IconButton, TeleportOverflowMenu } from '#ui/components/base/buttons'
|
||||
import { IconButton, TeleportOverflowMenu } from '#ui/components/base/buttons'
|
||||
import ErrorInformationCard from '#ui/components/base/ErrorInformationCard.vue'
|
||||
import NavTabs from '#ui/components/base/NavTabs.vue'
|
||||
import PageHeader from '#ui/components/base/page-header/index.vue'
|
||||
@@ -451,6 +359,10 @@ import {
|
||||
} from '#ui/composables'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { useServerBackupsQueue } from '#ui/composables/server-backups-queue'
|
||||
import type {
|
||||
ServerInstallationKey,
|
||||
ServerInstallationState,
|
||||
} from '#ui/composables/server-installation-tracker'
|
||||
import { useServerManageCoreRuntime } from '#ui/composables/server-manage-core-runtime'
|
||||
import { useServerPanelSync } from '#ui/composables/server-panel-sync'
|
||||
import type { LogLine } from '#ui/layouts/shared/console'
|
||||
@@ -463,11 +375,6 @@ import {
|
||||
import type { ServerStats } from '#ui/providers/server-context'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
import { formatLoaderLabel } from '#ui/utils/loaders'
|
||||
import {
|
||||
pendingServerContentInstallsEvent,
|
||||
readPendingServerContentInstalls,
|
||||
writePendingServerContentInstalls,
|
||||
} from '#ui/utils/server-content-installing'
|
||||
|
||||
import ServerOnboardingPanelPage from './[id]/onboarding.vue'
|
||||
|
||||
@@ -568,12 +475,6 @@ const debug = useDebugLogger('ServerManage')
|
||||
const isReconnecting = ref(false)
|
||||
const isLoading = ref(true)
|
||||
const isMounted = ref(true)
|
||||
const copied = ref(false)
|
||||
const installError = ref<Error | null>(null)
|
||||
const errorTitle = ref('Error')
|
||||
const errorMessage = ref('An unexpected error occurred.')
|
||||
const errorLog = ref('')
|
||||
const errorLogFile = ref('')
|
||||
const isOnboarding = computed(() => serverData.value?.flows?.intro)
|
||||
|
||||
const SETTINGS_HINT_KEY = 'server-panel-settings-hint-dismissed'
|
||||
@@ -627,6 +528,13 @@ const worldId = computed(() => {
|
||||
return activeWorld?.id ?? serverFull.value.worlds[0]?.id ?? null
|
||||
})
|
||||
|
||||
const { data: serverContent } = useQuery({
|
||||
queryKey: ['content', 'list', 'v1', props.serverId],
|
||||
queryFn: () =>
|
||||
client.archon.content_v1.getAddons(props.serverId, worldId.value!, { from_modpack: false }),
|
||||
enabled: computed(() => worldId.value !== null),
|
||||
})
|
||||
|
||||
const { handleWsBackupProgress, busyReasons: backupsBusy } = useServerBackupsQueue(
|
||||
computed(() => props.serverId),
|
||||
worldId,
|
||||
@@ -643,101 +551,25 @@ const { image: serverImage } = useServerImage(
|
||||
)
|
||||
const { data: serverProject } = useServerProject(computed(() => serverData.value?.upstream ?? null))
|
||||
|
||||
const syncProgress = ref<Archon.Websocket.v0.SyncContentProgress | null>(null)
|
||||
const contentError = ref<Archon.Websocket.v0.SyncContentError | null>(null)
|
||||
const syncProgressActive = ref(false)
|
||||
const hasPendingServerContentInstalls = ref(false)
|
||||
const hasSeenPendingServerContentSync = ref(false)
|
||||
const isAwaitingPostInstallRefresh = ref(false)
|
||||
const { start: startSyncHide, stop: cancelSyncHide } = useTimeoutFn(
|
||||
() => (syncProgressActive.value = false),
|
||||
1000,
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
watch(syncProgress, (progress) => {
|
||||
if (progress != null) {
|
||||
cancelSyncHide()
|
||||
syncProgressActive.value = true
|
||||
if (progress.phase !== 'Analyzing' && hasPendingServerContentInstalls.value) {
|
||||
hasSeenPendingServerContentSync.value = true
|
||||
}
|
||||
} else if (syncProgressActive.value) {
|
||||
startSyncHide()
|
||||
if (hasSeenPendingServerContentSync.value) {
|
||||
writePendingServerContentInstalls(props.serverId, worldId.value, [])
|
||||
hasSeenPendingServerContentSync.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(contentError, (error) => {
|
||||
if (!error || !hasPendingServerContentInstalls.value) return
|
||||
writePendingServerContentInstalls(props.serverId, worldId.value, [])
|
||||
hasSeenPendingServerContentSync.value = false
|
||||
})
|
||||
|
||||
const isSyncingContent = computed(
|
||||
() =>
|
||||
syncProgressActive.value ||
|
||||
isAwaitingPostInstallRefresh.value ||
|
||||
hasPendingServerContentInstalls.value,
|
||||
)
|
||||
|
||||
function syncPendingServerContentInstalls() {
|
||||
hasPendingServerContentInstalls.value =
|
||||
readPendingServerContentInstalls(props.serverId, worldId.value).length > 0
|
||||
}
|
||||
|
||||
function handlePendingServerContentInstallsChanged(event: Event) {
|
||||
const detail = (event as CustomEvent<{ serverId?: string | null; worldId?: string | null }>)
|
||||
.detail
|
||||
if (detail?.serverId !== props.serverId || detail?.worldId !== worldId.value) return
|
||||
syncPendingServerContentInstalls()
|
||||
}
|
||||
|
||||
watch(worldId, syncPendingServerContentInstalls, { immediate: true })
|
||||
|
||||
let hasSeenInstallProgress = false
|
||||
|
||||
const onStateEvent = (data: Archon.Websocket.v0.WSStateEvent) => {
|
||||
debug('[root.vue] handleState received:', {
|
||||
power_variant: data.power_variant,
|
||||
progress: data.progress,
|
||||
serverStatus: serverData.value?.status,
|
||||
})
|
||||
hasReceivedWsData.value = true
|
||||
syncProgress.value = data.progress
|
||||
contentError.value = data.content_error
|
||||
|
||||
if (serverData.value) {
|
||||
if (data.progress != null && serverData.value.status !== 'installing') {
|
||||
debug('[root.vue] handleState: progress != null, setting status to installing')
|
||||
hasSeenInstallProgress = true
|
||||
updateServerData({ status: 'installing' })
|
||||
} else if (data.progress != null) {
|
||||
hasSeenInstallProgress = true
|
||||
} else if (
|
||||
data.progress == null &&
|
||||
data.content_error == null &&
|
||||
serverData.value.status === 'installing' &&
|
||||
hasSeenInstallProgress
|
||||
) {
|
||||
debug('[root.vue] handleState: progress null + was installing, applying optimistic update')
|
||||
hasSeenInstallProgress = false
|
||||
applyOptimisticCompletion()
|
||||
invalidateAfterInstall()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
beginInstallation,
|
||||
cancelUpload,
|
||||
cancelOptimisticInstallation,
|
||||
cleanupCoreRuntime,
|
||||
connectSocket,
|
||||
cpuData,
|
||||
dismissInstallation,
|
||||
fsOps,
|
||||
fsQueuedOps,
|
||||
installation,
|
||||
isConnected,
|
||||
ramData,
|
||||
serverPowerState,
|
||||
@@ -749,7 +581,7 @@ const {
|
||||
worldId,
|
||||
server: serverData,
|
||||
serverFull,
|
||||
isSyncingContent,
|
||||
content: serverContent,
|
||||
extraBusyReasons: backupsBusy,
|
||||
setDisconnectedOnAuthIncorrect: false,
|
||||
syncUptimeFromState: true,
|
||||
@@ -1080,7 +912,7 @@ function loadTallyScript() {
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
async function handleContentRetry() {
|
||||
async function handleInstallationRetry() {
|
||||
if (!worldId.value) return
|
||||
if (!canSetup.value) {
|
||||
addNotification({
|
||||
@@ -1089,9 +921,16 @@ async function handleContentRetry() {
|
||||
})
|
||||
return
|
||||
}
|
||||
const failedInstallationId =
|
||||
installation.value?.status === 'failed' ? installation.value.id : null
|
||||
if (failedInstallationId) dismissInstallation(failedInstallationId)
|
||||
beginInstallation({ type: 'unknown' })
|
||||
updateServerData({ status: 'installing' })
|
||||
try {
|
||||
await client.archon.content_v1.repair(props.serverId, worldId.value)
|
||||
} catch (err) {
|
||||
cancelOptimisticInstallation()
|
||||
updateServerData({ status: 'available' })
|
||||
addNotification({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : 'Failed to retry installation',
|
||||
@@ -1133,54 +972,56 @@ const handleNewMod = () => {
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleInstallationResult = async (data: Archon.Websocket.v0.WSInstallationResultEvent) => {
|
||||
debug('[root.vue] handleInstallationResult received:', data)
|
||||
switch (data.result) {
|
||||
case 'ok': {
|
||||
debug('[root.vue] handleInstallationResult: ok received')
|
||||
if (!serverData.value) break
|
||||
type InstallationServerSnapshot = Pick<
|
||||
Archon.Servers.v0.Server,
|
||||
'loader' | 'loader_version' | 'mc_version'
|
||||
>
|
||||
|
||||
applyOptimisticCompletion()
|
||||
installError.value = null
|
||||
invalidateAfterInstall()
|
||||
let installationServerSnapshot: InstallationServerSnapshot | null = null
|
||||
|
||||
break
|
||||
}
|
||||
case 'err': {
|
||||
console.log('failed to install')
|
||||
console.log(data)
|
||||
errorTitle.value = 'Installation error'
|
||||
errorMessage.value = data.reason ?? 'Unknown error'
|
||||
installError.value = new Error(data.reason ?? 'Unknown error')
|
||||
function applyInstallationTarget(current: ServerInstallationState) {
|
||||
if (!serverData.value) return
|
||||
|
||||
try {
|
||||
let files = await client.kyros.files_v0.listDirectory('/', 1, 100)
|
||||
if (files && files.total > 1) {
|
||||
for (let i = 2; i <= files.total; i++) {
|
||||
const nextFiles = await client.kyros.files_v0.listDirectory('/', i, 100)
|
||||
if (nextFiles?.items?.length === 0) break
|
||||
if (nextFiles) files = nextFiles
|
||||
}
|
||||
}
|
||||
const fileName = files?.items?.find((file) =>
|
||||
file.name.startsWith('modrinth-installation'),
|
||||
)?.name
|
||||
errorLogFile.value = fileName ?? ''
|
||||
if (fileName) {
|
||||
const content = await client.kyros.files_v0.downloadFile(fileName)
|
||||
errorLog.value = await content.text()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch installation log:', err)
|
||||
}
|
||||
break
|
||||
if (!installationServerSnapshot) {
|
||||
installationServerSnapshot = {
|
||||
loader: serverData.value.loader,
|
||||
loader_version: serverData.value.loader_version,
|
||||
mc_version: serverData.value.mc_version,
|
||||
}
|
||||
}
|
||||
|
||||
const patch: Partial<Archon.Servers.v0.Server> = { status: 'installing' }
|
||||
if (current.key.type === 'platform') {
|
||||
patch.loader = formatLoaderLabel(current.key.platform) as Archon.Servers.v0.Loader
|
||||
patch.loader_version = current.key.platform === 'vanilla' ? null : current.key.platform_version
|
||||
patch.mc_version = current.key.game_version
|
||||
}
|
||||
|
||||
if (
|
||||
serverData.value.status === patch.status &&
|
||||
(current.key.type !== 'platform' ||
|
||||
(serverData.value.loader === patch.loader &&
|
||||
serverData.value.loader_version === patch.loader_version &&
|
||||
serverData.value.mc_version === patch.mc_version))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
void queryClient.cancelQueries({
|
||||
queryKey: ['servers', 'detail', props.serverId],
|
||||
exact: true,
|
||||
})
|
||||
updateServerData(patch)
|
||||
}
|
||||
|
||||
const newLoader = ref<string | null>(null)
|
||||
const newLoaderVersion = ref<string | null>(null)
|
||||
const newMCVersion = ref<string | null>(null)
|
||||
function restoreInstallationServerSnapshot() {
|
||||
const snapshot = installationServerSnapshot
|
||||
updateServerData({
|
||||
...(snapshot ?? {}),
|
||||
status: 'available',
|
||||
})
|
||||
installationServerSnapshot = null
|
||||
}
|
||||
|
||||
const onReinstall = async (
|
||||
potentialArgs: { loader?: string; lVersion?: string; mVersion?: string } | undefined,
|
||||
@@ -1194,70 +1035,63 @@ const onReinstall = async (
|
||||
|
||||
if (!serverData.value) return
|
||||
|
||||
debug('[root.vue] onReinstall: setting serverData.status to installing')
|
||||
hasSeenInstallProgress = false
|
||||
updateServerData({ status: 'installing' })
|
||||
|
||||
if (potentialArgs?.loader) {
|
||||
newLoader.value = potentialArgs.loader
|
||||
if (
|
||||
!installation.value ||
|
||||
installation.value.status === 'complete' ||
|
||||
installation.value.status === 'failed'
|
||||
) {
|
||||
if (potentialArgs?.loader && potentialArgs.mVersion) {
|
||||
beginInstallation({
|
||||
type: 'platform',
|
||||
platform: potentialArgs.loader as Extract<
|
||||
Archon.Websocket.v0.InstallProgressKey,
|
||||
{ type: 'platform' }
|
||||
>['platform'],
|
||||
platform_version: potentialArgs.lVersion ?? '',
|
||||
game_version: potentialArgs.mVersion,
|
||||
})
|
||||
} else {
|
||||
beginInstallation({ type: 'unknown' })
|
||||
}
|
||||
}
|
||||
if (potentialArgs?.lVersion) {
|
||||
newLoaderVersion.value = potentialArgs.lVersion
|
||||
}
|
||||
if (potentialArgs?.mVersion) {
|
||||
newMCVersion.value = potentialArgs.mVersion
|
||||
}
|
||||
|
||||
installError.value = null
|
||||
errorTitle.value = 'Error'
|
||||
errorMessage.value = 'An unexpected error occurred.'
|
||||
|
||||
modrinthServersConsole.clear()
|
||||
|
||||
debug('[root.vue] onReinstall: triggering immediate invalidation')
|
||||
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', props.serverId] })
|
||||
queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
|
||||
}
|
||||
|
||||
const onReinstallFailed = () => {
|
||||
debug('[root.vue] onReinstallFailed: reverting status to available')
|
||||
updateServerData({ status: 'available' })
|
||||
newLoader.value = null
|
||||
newLoaderVersion.value = null
|
||||
newMCVersion.value = null
|
||||
cancelOptimisticInstallation()
|
||||
restoreInstallationServerSnapshot()
|
||||
}
|
||||
|
||||
function applyOptimisticCompletion() {
|
||||
function applyInstallationCompletion(key: ServerInstallationKey) {
|
||||
const platformKey = key?.type === 'platform' ? key : null
|
||||
const patch: Partial<Archon.Servers.v0.Server> = { status: 'available' }
|
||||
if (newLoader.value) patch.loader = formatLoaderLabel(newLoader.value) as Archon.Servers.v0.Loader
|
||||
if (newLoaderVersion.value) patch.loader_version = newLoaderVersion.value
|
||||
if (newMCVersion.value) patch.mc_version = newMCVersion.value
|
||||
if (platformKey) {
|
||||
patch.loader = formatLoaderLabel(platformKey.platform) as Archon.Servers.v0.Loader
|
||||
patch.loader_version = platformKey.platform === 'vanilla' ? null : platformKey.platform_version
|
||||
patch.mc_version = platformKey.game_version
|
||||
}
|
||||
|
||||
debug('[root.vue] applyOptimisticCompletion: patch:', patch)
|
||||
debug('[root.vue] applyInstallationCompletion: patch:', patch)
|
||||
updateServerData(patch)
|
||||
|
||||
const addonsQueries = queryClient.getQueriesData<Archon.Content.v1.Addons>({
|
||||
queryKey: ['content', 'list', 'v1', props.serverId],
|
||||
})
|
||||
for (const [key, data] of addonsQueries) {
|
||||
if (!data) continue
|
||||
const addonsPatch: Record<string, string> = {}
|
||||
if (newLoader.value) addonsPatch.modloader = newLoader.value
|
||||
if (newLoaderVersion.value) addonsPatch.modloader_version = newLoaderVersion.value
|
||||
if (newMCVersion.value) addonsPatch.game_version = newMCVersion.value
|
||||
if (Object.keys(addonsPatch).length > 0) {
|
||||
queryClient.setQueryData(key, { ...data, ...addonsPatch })
|
||||
}
|
||||
if (!data || !platformKey) continue
|
||||
queryClient.setQueryData(key, {
|
||||
...data,
|
||||
modloader: platformKey.platform === 'neoforge' ? 'neo_forge' : platformKey.platform,
|
||||
modloader_version: platformKey.platform === 'vanilla' ? null : platformKey.platform_version,
|
||||
game_version: platformKey.game_version,
|
||||
})
|
||||
}
|
||||
|
||||
newLoader.value = null
|
||||
newLoaderVersion.value = null
|
||||
newMCVersion.value = null
|
||||
}
|
||||
|
||||
async function invalidateAfterInstall() {
|
||||
debug('[root.vue] invalidateAfterInstall: scheduling 2s delayed invalidation')
|
||||
isAwaitingPostInstallRefresh.value = true
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
@@ -1269,12 +1103,48 @@ async function invalidateAfterInstall() {
|
||||
])
|
||||
} catch (err: unknown) {
|
||||
console.error('Error refreshing data after installation:', err)
|
||||
} finally {
|
||||
isAwaitingPostInstallRefresh.value = false
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
let handledFailedInstallationId: string | null = null
|
||||
watch(
|
||||
installation,
|
||||
(current, previous) => {
|
||||
if (!current) {
|
||||
if (
|
||||
isMounted.value &&
|
||||
previous?.source === 'optimistic' &&
|
||||
previous.status === 'pending' &&
|
||||
serverData.value?.status === 'installing'
|
||||
) {
|
||||
restoreInstallationServerSnapshot()
|
||||
}
|
||||
return
|
||||
}
|
||||
if (current.status === 'pending' || current.status === 'installing') {
|
||||
handledFailedInstallationId = null
|
||||
applyInstallationTarget(current)
|
||||
return
|
||||
}
|
||||
|
||||
if (current.status === 'failed') {
|
||||
if (handledFailedInstallationId === current.id) return
|
||||
handledFailedInstallationId = current.id
|
||||
if (current.source === 'server') return
|
||||
onReinstallFailed()
|
||||
void invalidateAfterInstall()
|
||||
return
|
||||
}
|
||||
|
||||
applyInstallationCompletion(current.key)
|
||||
installationServerSnapshot = null
|
||||
dismissInstallation(current.id)
|
||||
void invalidateAfterInstall()
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
)
|
||||
|
||||
const nodeAccessible = ref(true)
|
||||
|
||||
const nodeUnavailableDetails = computed(() => [
|
||||
@@ -1295,7 +1165,7 @@ const nodeUnavailableDetails = computed(() => [
|
||||
label: 'Error message',
|
||||
value: nodeAccessible.value
|
||||
? (serverError.value?.message ?? 'Unknown')
|
||||
: 'Unable to reach node. Ping test failed.',
|
||||
: 'Unable to establish the node WebSocket connection.',
|
||||
type: 'block' as const,
|
||||
},
|
||||
])
|
||||
@@ -1370,21 +1240,6 @@ const nodeUnavailableAction = computed(() => ({
|
||||
disabled: false,
|
||||
}))
|
||||
|
||||
const copyServerDebugInfo = () => {
|
||||
const debugInfo = `Server ID: ${serverData.value?.server_id}\nError: ${errorMessage.value}\nKind: ${serverData.value?.upstream?.kind}\nProject ID: ${serverData.value?.upstream?.project_id}\nVersion ID: ${serverData.value?.upstream?.version_id}\nLog: ${errorLog.value}`
|
||||
navigator.clipboard.writeText(debugInfo)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
const openInstallLog = () => {
|
||||
const url = `/hosting/manage/${props.serverId}/files?editing=${encodeURIComponent(errorLogFile.value)}`
|
||||
window.history.pushState({}, '', url)
|
||||
window.dispatchEvent(new PopStateEvent('popstate'))
|
||||
}
|
||||
|
||||
function openServerSettingsModal(tabId?: ServerSettingsTabId) {
|
||||
if (!props.serverId) return
|
||||
serverSettingsModal.value?.show({ serverId: props.serverId, tabId })
|
||||
@@ -1428,48 +1283,6 @@ function safeStringify(obj: unknown, indent = ' '): string {
|
||||
)
|
||||
}
|
||||
|
||||
async function testNodeReachability(): Promise<boolean> {
|
||||
const nodeInstance = serverData.value?.node?.instance
|
||||
if (!nodeInstance) return false
|
||||
|
||||
try {
|
||||
const auth = await client.archon.servers_v0.getWebSocketAuth(props.serverId)
|
||||
const authUrl = getNodeWebSocketUrl(auth.url)
|
||||
const protocol = authUrl.toLowerCase().startsWith('ws://') ? 'ws' : 'wss'
|
||||
const wsUrl = getNodeWebSocketUrl(`${nodeInstance}/pingtest`).replace(
|
||||
/^wss?:\/\//i,
|
||||
`${protocol}://`,
|
||||
)
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
const socket = new WebSocket(wsUrl)
|
||||
const timeout = setTimeout(() => {
|
||||
socket.close()
|
||||
resolve(false)
|
||||
}, 5000)
|
||||
|
||||
socket.onopen = () => {
|
||||
clearTimeout(timeout)
|
||||
socket.send(performance.now().toString())
|
||||
}
|
||||
|
||||
socket.onmessage = () => {
|
||||
clearTimeout(timeout)
|
||||
socket.close()
|
||||
resolve(true)
|
||||
}
|
||||
|
||||
socket.onerror = () => {
|
||||
clearTimeout(timeout)
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(`Failed to ping node ${nodeInstance}:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function initializeServer() {
|
||||
if (serverData.value?.status === 'suspended') {
|
||||
isLoading.value = false
|
||||
@@ -1481,31 +1294,18 @@ function initializeServer() {
|
||||
return
|
||||
}
|
||||
|
||||
testNodeReachability()
|
||||
.then((result) => {
|
||||
nodeAccessible.value = result
|
||||
if (!nodeAccessible.value) {
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error testing node reachability:', err)
|
||||
nodeAccessible.value = false
|
||||
isLoading.value = false
|
||||
})
|
||||
|
||||
if (serverError.value) {
|
||||
isLoading.value = false
|
||||
} else {
|
||||
void connectSocket(props.serverId, {
|
||||
extraSubscriptions: (targetServerId) => [
|
||||
client.archon.sockets.on(targetServerId, 'installation-result', handleInstallationResult),
|
||||
client.archon.sockets.on(targetServerId, 'backup-progress', handleBackupProgress),
|
||||
client.archon.sockets.on(targetServerId, 'filesystem-ops', handleFilesystemOps),
|
||||
client.archon.sockets.on(targetServerId, 'new-mod', handleNewMod),
|
||||
],
|
||||
})
|
||||
.then((connected) => {
|
||||
nodeAccessible.value = connected
|
||||
if (connected && cachedWsState?.consoleLines?.length) {
|
||||
modrinthServersConsole.clear()
|
||||
modrinthServersConsole.addLines(cachedWsState.consoleLines)
|
||||
@@ -1543,11 +1343,6 @@ const cleanup = () => {
|
||||
|
||||
onMounted(() => {
|
||||
isMounted.value = true
|
||||
syncPendingServerContentInstalls()
|
||||
window.addEventListener(
|
||||
pendingServerContentInstallsEvent,
|
||||
handlePendingServerContentInstallsChanged,
|
||||
)
|
||||
|
||||
if (serverData.value) {
|
||||
initializeServer()
|
||||
@@ -1589,10 +1384,6 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener(
|
||||
pendingServerContentInstallsEvent,
|
||||
handlePendingServerContentInstallsChanged,
|
||||
)
|
||||
cleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user