mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 09:34:50 +00:00
Revert "Revert "feat: sync individual content installation states on … (#7155)
Revert "Revert "feat: sync individual content installation states on panel" (…"
This reverts commit 1dba3bfee8.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<Admonition
|
||||
:type="contentError ? 'critical' : 'info'"
|
||||
:dismissible="dismissible"
|
||||
v-if="installation"
|
||||
:type="installation.status === 'failed' ? 'critical' : 'info'"
|
||||
:dismissible="installation.status === 'failed'"
|
||||
:progress="progressValue"
|
||||
progress-color="blue"
|
||||
:waiting="isWaiting"
|
||||
@@ -10,23 +11,8 @@
|
||||
<template #header>
|
||||
{{ headerLabel }}
|
||||
</template>
|
||||
<template v-if="contentError">
|
||||
{{ errorLabel }}
|
||||
</template>
|
||||
<template v-else-if="effectivePhase">{{ phaseLabel }}</template>
|
||||
<div v-else class="ticker-container">
|
||||
<div class="ticker-content">
|
||||
<div
|
||||
v-for="(message, index) in tickerMessages"
|
||||
:key="message"
|
||||
class="ticker-item"
|
||||
:class="{ active: index === currentIndex % tickerMessages.length }"
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="contentError" #top-right-actions>
|
||||
{{ installation.status === 'failed' ? errorLabel : descriptionLabel }}
|
||||
<template v-if="installation.status === 'failed'" #top-right-actions>
|
||||
<Button
|
||||
v-tooltip="retryDisabled ? retryDisabledTooltip : undefined"
|
||||
type="outlined"
|
||||
@@ -44,29 +30,17 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RotateCounterClockwiseIcon } from '@modrinth/assets'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { Button } from '#ui/components/base/buttons'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { injectModrinthServerContext } from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
import { formatLoaderLabel } from '#ui/utils/loaders'
|
||||
|
||||
import Admonition from '../base/Admonition.vue'
|
||||
|
||||
export interface SyncProgress {
|
||||
phase: 'Analyzing' | 'InstallingPack' | 'InstallingLoader' | 'Addons'
|
||||
percent: number
|
||||
}
|
||||
|
||||
export interface ContentError {
|
||||
step: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
progress?: SyncProgress | null
|
||||
fallbackPhase?: SyncProgress['phase'] | null
|
||||
contentError?: ContentError | null
|
||||
dismissible?: boolean
|
||||
defineProps<{
|
||||
retryDisabled?: boolean
|
||||
retryDisabledTooltip?: string
|
||||
}>()
|
||||
@@ -77,191 +51,182 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { installation } = injectModrinthServerContext()
|
||||
|
||||
const messages = defineMessages({
|
||||
errorHeader: {
|
||||
id: 'servers.installing-banner.error.header',
|
||||
defaultMessage: 'Installation failed',
|
||||
},
|
||||
preparingHeader: {
|
||||
id: 'servers.installing-banner.preparing.header',
|
||||
defaultMessage: "We're preparing your server",
|
||||
platformErrorHeader: {
|
||||
id: 'servers.installing-banner.error.header.platform',
|
||||
defaultMessage: 'Failed to install {loader} {loaderVersion} for Minecraft {gameVersion}',
|
||||
},
|
||||
invalidLoaderVersionError: {
|
||||
id: 'servers.installing-banner.error.invalid-loader-version',
|
||||
defaultMessage:
|
||||
'The specified loader or Minecraft version could not be installed. It may be invalid or unsupported.',
|
||||
platformWithoutVersionErrorHeader: {
|
||||
id: 'servers.installing-banner.error.header.platform-without-version',
|
||||
defaultMessage: 'Failed to install {loader} for Minecraft {gameVersion}',
|
||||
},
|
||||
unsupportedLoaderVersionError: {
|
||||
id: 'servers.installing-banner.error.unsupported-loader-version',
|
||||
defaultMessage: 'This version of Minecraft or loader is not yet supported by Modrinth Hosting.',
|
||||
minecraftErrorHeader: {
|
||||
id: 'servers.installing-banner.error.header.minecraft',
|
||||
defaultMessage: 'Failed to install Minecraft {version}',
|
||||
},
|
||||
internalPlatformError: {
|
||||
id: 'servers.installing-banner.error.internal-platform',
|
||||
defaultMessage: 'An internal error occurred while installing the platform. Please try again.',
|
||||
modpackErrorHeader: {
|
||||
id: 'servers.installing-banner.error.header.modpack',
|
||||
defaultMessage: 'Failed to install modpack',
|
||||
},
|
||||
noPrimaryFileError: {
|
||||
id: 'servers.installing-banner.error.no-primary-file',
|
||||
defaultMessage:
|
||||
'This modpack version does not include a downloadable file. It may have been packaged incorrectly.',
|
||||
},
|
||||
modpackInstallFailedError: {
|
||||
id: 'servers.installing-banner.error.modpack-install-failed',
|
||||
defaultMessage: 'The modpack could not be installed. It may be corrupted or incompatible.',
|
||||
localModpackErrorHeader: {
|
||||
id: 'servers.installing-banner.error.header.local-modpack',
|
||||
defaultMessage: 'Failed to install {filename}',
|
||||
},
|
||||
unknownError: {
|
||||
id: 'servers.installing-banner.error.unknown',
|
||||
defaultMessage: 'An unexpected error occurred during installation.',
|
||||
},
|
||||
preparingHeader: {
|
||||
id: 'servers.installing-banner.preparing.header',
|
||||
defaultMessage: 'Preparing your server',
|
||||
},
|
||||
installingPlatform: {
|
||||
id: 'servers.installing-banner.phase.installing-platform',
|
||||
defaultMessage: 'Installing platform...',
|
||||
id: 'servers.installing-banner.installing-platform',
|
||||
defaultMessage: 'Installing {loader} for Minecraft {version}',
|
||||
},
|
||||
installingMinecraft: {
|
||||
id: 'servers.installing-banner.installing-minecraft',
|
||||
defaultMessage: 'Installing Minecraft {version}',
|
||||
},
|
||||
installingModpack: {
|
||||
id: 'servers.installing-banner.phase.installing-modpack',
|
||||
defaultMessage: 'Installing modpack...',
|
||||
id: 'servers.installing-banner.installing-modpack',
|
||||
defaultMessage: 'Installing modpack',
|
||||
},
|
||||
installingAddons: {
|
||||
id: 'servers.installing-banner.phase.installing-addons',
|
||||
defaultMessage: 'Installing addons...',
|
||||
installingLocalModpack: {
|
||||
id: 'servers.installing-banner.installing-local-modpack',
|
||||
defaultMessage: 'Installing {filename}',
|
||||
},
|
||||
tickerOrganizingFiles: {
|
||||
id: 'servers.installing-banner.ticker.organizing-files',
|
||||
defaultMessage: 'Organizing files...',
|
||||
preparingDescription: {
|
||||
id: 'servers.installing-banner.description.preparing',
|
||||
defaultMessage: 'Preparing your server...',
|
||||
},
|
||||
tickerDownloadingMods: {
|
||||
id: 'servers.installing-banner.ticker.downloading-mods',
|
||||
defaultMessage: 'Downloading mods...',
|
||||
applyingDescription: {
|
||||
id: 'servers.installing-banner.description.applying',
|
||||
defaultMessage: 'Applying your installation changes...',
|
||||
},
|
||||
tickerConfiguringServer: {
|
||||
id: 'servers.installing-banner.ticker.configuring-server',
|
||||
defaultMessage: 'Configuring server...',
|
||||
durationDescription: {
|
||||
id: 'servers.installing-banner.description.duration',
|
||||
defaultMessage: 'This installation may take several minutes...',
|
||||
},
|
||||
tickerSettingUpEnvironment: {
|
||||
id: 'servers.installing-banner.ticker.setting-up-environment',
|
||||
defaultMessage: 'Setting up environment...',
|
||||
controlsDescription: {
|
||||
id: 'servers.installing-banner.description.controls',
|
||||
defaultMessage: 'Server controls will unlock when installation finishes.',
|
||||
},
|
||||
tickerAddingJava: {
|
||||
id: 'servers.installing-banner.ticker.adding-java',
|
||||
defaultMessage: 'Adding Java...',
|
||||
stillWorkingDescription: {
|
||||
id: 'servers.installing-banner.description.still-working',
|
||||
defaultMessage: 'Still working—your installation is in progress...',
|
||||
},
|
||||
})
|
||||
|
||||
const errorLabel = computed(() => {
|
||||
const desc = props.contentError?.description?.toLowerCase()
|
||||
const step = props.contentError?.step
|
||||
|
||||
if (step === 'modloader') {
|
||||
if (desc === 'the specified version may be incorrect') {
|
||||
return formatMessage(messages.invalidLoaderVersionError)
|
||||
}
|
||||
if (desc === 'this version is not yet supported') {
|
||||
return formatMessage(messages.unsupportedLoaderVersionError)
|
||||
}
|
||||
if (desc === 'internal error') {
|
||||
return formatMessage(messages.internalPlatformError)
|
||||
}
|
||||
}
|
||||
|
||||
if (step === 'modpack') {
|
||||
if (desc?.includes('no primary file')) {
|
||||
return formatMessage(messages.noPrimaryFileError)
|
||||
}
|
||||
if (desc?.includes('failed to install')) {
|
||||
return formatMessage(messages.modpackInstallFailedError)
|
||||
}
|
||||
}
|
||||
|
||||
return props.contentError?.description ?? formatMessage(messages.unknownError)
|
||||
})
|
||||
|
||||
const effectivePhase = computed(() => props.progress?.phase ?? props.fallbackPhase ?? null)
|
||||
|
||||
const headerLabel = computed(() => {
|
||||
if (props.contentError) return formatMessage(messages.errorHeader)
|
||||
if (effectivePhase.value === 'Addons') return formatMessage(commonMessages.installingContentLabel)
|
||||
const current = installation.value
|
||||
if (!current) return ''
|
||||
|
||||
switch (current.key.type) {
|
||||
case 'platform': {
|
||||
if (current.key.platform === 'vanilla') {
|
||||
if (current.status === 'failed') {
|
||||
return formatMessage(messages.minecraftErrorHeader, {
|
||||
version: current.key.game_version,
|
||||
})
|
||||
}
|
||||
return formatMessage(messages.installingMinecraft, {
|
||||
version: current.key.game_version,
|
||||
})
|
||||
}
|
||||
if (current.status === 'failed') {
|
||||
if (!current.key.platform_version) {
|
||||
return formatMessage(messages.platformWithoutVersionErrorHeader, {
|
||||
loader: formatLoaderLabel(current.key.platform),
|
||||
gameVersion: current.key.game_version,
|
||||
})
|
||||
}
|
||||
return formatMessage(messages.platformErrorHeader, {
|
||||
loader: formatLoaderLabel(current.key.platform),
|
||||
loaderVersion: current.key.platform_version,
|
||||
gameVersion: current.key.game_version,
|
||||
})
|
||||
}
|
||||
return formatMessage(messages.installingPlatform, {
|
||||
loader: formatLoaderLabel(current.key.platform),
|
||||
version: current.key.game_version,
|
||||
})
|
||||
}
|
||||
case 'modrinth_modpack':
|
||||
return formatMessage(
|
||||
current.status === 'failed' ? messages.modpackErrorHeader : messages.installingModpack,
|
||||
)
|
||||
case 'local_modpack':
|
||||
return formatMessage(
|
||||
current.status === 'failed'
|
||||
? messages.localModpackErrorHeader
|
||||
: messages.installingLocalModpack,
|
||||
{
|
||||
filename: current.key.filename,
|
||||
},
|
||||
)
|
||||
case 'unknown':
|
||||
return formatMessage(
|
||||
current.status === 'failed' ? messages.errorHeader : messages.preparingHeader,
|
||||
)
|
||||
}
|
||||
|
||||
return formatMessage(messages.preparingHeader)
|
||||
})
|
||||
|
||||
const phaseLabel = computed(() => {
|
||||
switch (effectivePhase.value) {
|
||||
case 'InstallingLoader':
|
||||
return formatMessage(messages.installingPlatform)
|
||||
case 'InstallingPack':
|
||||
return formatMessage(messages.installingModpack)
|
||||
case 'Addons':
|
||||
return formatMessage(messages.installingAddons)
|
||||
const descriptionIndex = ref(0)
|
||||
const installationId = computed(() => installation.value?.id ?? null)
|
||||
|
||||
watch(installationId, () => {
|
||||
descriptionIndex.value = 0
|
||||
})
|
||||
|
||||
const descriptionLabel = computed(() => {
|
||||
switch (descriptionIndex.value) {
|
||||
case 0:
|
||||
return formatMessage(messages.preparingDescription)
|
||||
case 1:
|
||||
return formatMessage(messages.applyingDescription)
|
||||
case 2:
|
||||
return formatMessage(messages.durationDescription)
|
||||
default:
|
||||
return formatMessage(commonMessages.installingLabel)
|
||||
return descriptionIndex.value % 2 === 1
|
||||
? formatMessage(messages.controlsDescription)
|
||||
: formatMessage(messages.stillWorkingDescription)
|
||||
}
|
||||
})
|
||||
|
||||
const errorLabel = computed(() => installation.value?.error ?? formatMessage(messages.unknownError))
|
||||
|
||||
const progressValue = computed(() => {
|
||||
if (props.contentError) return undefined
|
||||
return props.progress ? props.progress.percent / 100 : 0
|
||||
const current = installation.value
|
||||
if (!current || current.status === 'failed') return undefined
|
||||
return current.progress == null ? 0 : current.progress / 100
|
||||
})
|
||||
|
||||
const isWaiting = computed(() => {
|
||||
if (props.contentError) return false
|
||||
return !props.progress || props.progress.percent <= 0
|
||||
const current = installation.value
|
||||
if (!current || current.status === 'failed') return false
|
||||
return current.progress == null || current.progress <= 0
|
||||
})
|
||||
|
||||
const tickerMessages = computed(() => [
|
||||
formatMessage(messages.tickerOrganizingFiles),
|
||||
formatMessage(messages.tickerDownloadingMods),
|
||||
formatMessage(messages.tickerConfiguringServer),
|
||||
formatMessage(messages.tickerSettingUpEnvironment),
|
||||
formatMessage(messages.tickerAddingJava),
|
||||
])
|
||||
|
||||
const currentIndex = ref(0)
|
||||
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
onMounted(() => {
|
||||
intervalId = setInterval(() => {
|
||||
currentIndex.value = (currentIndex.value + 1) % tickerMessages.value.length
|
||||
}, 3000)
|
||||
if (installation.value?.status === 'pending' || installation.value?.status === 'installing') {
|
||||
descriptionIndex.value += 1
|
||||
}
|
||||
}, 15_000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
if (intervalId) clearInterval(intervalId)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ticker-container {
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ticker-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ticker-item {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
color: var(--color-secondary-text);
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
filter: blur(4px);
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.ticker-item.active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
filter: blur(0);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -131,6 +131,11 @@ async function onFlowComplete(ctx: CreationFlowContextValue) {
|
||||
await handleMrpackUpload(ctx.modpackFile.value, ctx.buildProperties())
|
||||
} else if (ctx.setupType.value === 'modpack' && ctx.modpackSelection.value) {
|
||||
debug('onFlowComplete: modpack selection path, calling installContent')
|
||||
serverContext.beginInstallation({
|
||||
type: 'modrinth_modpack',
|
||||
project_id: ctx.modpackSelection.value.projectId,
|
||||
version_id: ctx.modpackSelection.value.versionId,
|
||||
})
|
||||
await client.archon.content_v1.installContent(
|
||||
serverContext.serverId,
|
||||
serverContext.worldId.value!,
|
||||
@@ -159,6 +164,15 @@ async function onFlowComplete(ctx: CreationFlowContextValue) {
|
||||
apiLoader: toApiLoader(loader ?? 'vanilla'),
|
||||
})
|
||||
|
||||
serverContext.beginInstallation({
|
||||
type: 'platform',
|
||||
platform: (loader ?? 'vanilla') as Extract<
|
||||
Archon.Websocket.v0.InstallProgressKey,
|
||||
{ type: 'platform' }
|
||||
>['platform'],
|
||||
platform_version: loaderVersion,
|
||||
game_version: ctx.selectedGameVersion.value ?? '',
|
||||
})
|
||||
await client.archon.content_v1.installContent(
|
||||
serverContext.serverId,
|
||||
serverContext.worldId.value!,
|
||||
@@ -183,6 +197,7 @@ async function onFlowComplete(ctx: CreationFlowContextValue) {
|
||||
creationFlowRef.value?.hide()
|
||||
} catch (error) {
|
||||
debug('onFlowComplete: ERROR', error)
|
||||
serverContext.cancelOptimisticInstallation()
|
||||
if ((error as ModrinthApiError).statusCode === 429) {
|
||||
addNotification({
|
||||
title: formatMessage(messages.rateLimitTitle),
|
||||
@@ -210,6 +225,10 @@ async function handleMrpackUpload(file: File, properties: Archon.Content.v1.Prop
|
||||
{ softOverride: false },
|
||||
)
|
||||
await uploadProgressModal.value!.track(handle)
|
||||
serverContext.beginInstallation({
|
||||
type: 'local_modpack',
|
||||
filename: file.name,
|
||||
})
|
||||
emitReinstall()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import Admonition from '#ui/components/base/Admonition.vue'
|
||||
import StackedAdmonitions, {
|
||||
type StackedAdmonitionItem,
|
||||
} from '#ui/components/base/StackedAdmonitions.vue'
|
||||
import InstallingBanner, {
|
||||
type ContentError,
|
||||
type SyncProgress,
|
||||
} from '#ui/components/servers/InstallingBanner.vue'
|
||||
import InstallingBanner from '#ui/components/servers/InstallingBanner.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { useServerBackupsQueue } from '#ui/composables/server-backups-queue'
|
||||
import { useServerPermissions } from '#ui/composables/server-permissions'
|
||||
@@ -20,13 +17,8 @@ import BackupAdmonition, { type BackupAdmonitionEntry } from './BackupAdmonition
|
||||
import FileOperationAdmonition from './FileOperationAdmonition.vue'
|
||||
import UploadAdmonition from './UploadAdmonition.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
syncProgress?: SyncProgress | null
|
||||
contentError?: ContentError | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'content-retry': []
|
||||
'installation-retry': []
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -58,28 +50,18 @@ const messages = defineMessages({
|
||||
const isOnContentTab = computed(() => route.path.includes('/content'))
|
||||
const isOnFilesTab = computed(() => route.path.includes('/files'))
|
||||
|
||||
const bannerCoversInstalling = computed(
|
||||
() =>
|
||||
ctx.server.value?.status === 'installing' ||
|
||||
ctx.isSyncingContent.value ||
|
||||
ctx.busyReasons.value.some(
|
||||
(r) =>
|
||||
r.reason.id === 'servers.busy.installing' || r.reason.id === 'servers.busy.syncing-content',
|
||||
),
|
||||
)
|
||||
|
||||
function isBackupReason(id: string) {
|
||||
return id === 'servers.busy.backup-creating' || id === 'servers.busy.backup-restoring'
|
||||
}
|
||||
|
||||
function isInstallingReason(id: string) {
|
||||
return id === 'servers.busy.installing' || id === 'servers.busy.syncing-content'
|
||||
return id === 'servers.busy.installing'
|
||||
}
|
||||
|
||||
const filteredBusyReasons = computed(() =>
|
||||
ctx.busyReasons.value.filter((r) => {
|
||||
if (isBackupReason(r.reason.id)) return false
|
||||
if (bannerCoversInstalling.value && isInstallingReason(r.reason.id)) return false
|
||||
if (isInstallingReason(r.reason.id)) return false
|
||||
return true
|
||||
}),
|
||||
)
|
||||
@@ -95,17 +77,6 @@ const filesBusyHeader = computed(() =>
|
||||
const dismissedIds = reactive(new Set<string>())
|
||||
const cancellingIds = reactive(new Set<string>())
|
||||
const uploadCancelling = ref(false)
|
||||
const dismissedContentErrorKey = ref<string | null>(null)
|
||||
|
||||
const contentErrorKey = computed(() =>
|
||||
props.contentError ? `${props.contentError.step}:${props.contentError.description}` : null,
|
||||
)
|
||||
|
||||
watch(contentErrorKey, (key) => {
|
||||
if (!key) {
|
||||
dismissedContentErrorKey.value = null
|
||||
}
|
||||
})
|
||||
|
||||
const backupAdmonitionEntries = computed<BackupAdmonitionEntry[]>(() => {
|
||||
const result: BackupAdmonitionEntry[] = []
|
||||
@@ -171,12 +142,7 @@ type ServerAdmonitionItem = StackedAdmonitionItem & {
|
||||
)
|
||||
|
||||
const showInstallingBanner = computed(() => {
|
||||
if (!ctx.server.value) return false
|
||||
const installing = bannerCoversInstalling.value || !!props.contentError
|
||||
if (!installing) return false
|
||||
if (contentErrorKey.value && dismissedContentErrorKey.value === contentErrorKey.value)
|
||||
return false
|
||||
return props.syncProgress?.phase !== 'Analyzing'
|
||||
return !!ctx.installation.value && ctx.installation.value.status !== 'complete'
|
||||
})
|
||||
|
||||
function fsOpType(op: FileOperation): StackedAdmonitionItem['type'] {
|
||||
@@ -210,10 +176,11 @@ const stackItems = computed<ServerAdmonitionItem[]>(() => {
|
||||
let sortIndex = 0
|
||||
|
||||
if (showInstallingBanner.value) {
|
||||
const failed = ctx.installation.value?.status === 'failed'
|
||||
out.push({
|
||||
id: 'installing',
|
||||
type: props.contentError ? 'critical' : 'info',
|
||||
dismissible: !!props.contentError,
|
||||
type: failed ? 'critical' : 'info',
|
||||
dismissible: failed,
|
||||
kind: 'installing',
|
||||
priority: 0,
|
||||
sortIndex: sortIndex++,
|
||||
@@ -365,8 +332,8 @@ async function onDismissAll() {
|
||||
const tasks: Promise<unknown>[] = []
|
||||
for (const it of stackItems.value) {
|
||||
if (!it.dismissible) continue
|
||||
if (it.kind === 'installing' && props.contentError) {
|
||||
onContentErrorDismiss()
|
||||
if (it.kind === 'installing') {
|
||||
onInstallationDismiss()
|
||||
} else if (it.kind === 'fs-op' && it.op.id) {
|
||||
const { op } = it
|
||||
if (op.state === 'done' || op.state?.startsWith('fail')) {
|
||||
@@ -385,9 +352,9 @@ function onFileOpDismiss(item: ServerAdmonitionItem) {
|
||||
}
|
||||
}
|
||||
|
||||
function onContentErrorDismiss() {
|
||||
if (contentErrorKey.value) {
|
||||
dismissedContentErrorKey.value = contentErrorKey.value
|
||||
function onInstallationDismiss() {
|
||||
if (ctx.installation.value) {
|
||||
ctx.dismissInstallation(ctx.installation.value.id)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -402,14 +369,10 @@ function onContentErrorDismiss() {
|
||||
<template #item="{ item, dismissible }">
|
||||
<InstallingBanner
|
||||
v-if="item.kind === 'installing'"
|
||||
:progress="syncProgress"
|
||||
:fallback-phase="isOnContentTab && !syncProgress ? 'Addons' : null"
|
||||
:content-error="contentError"
|
||||
:dismissible="dismissible && !!contentError"
|
||||
:retry-disabled="!canSetup"
|
||||
:retry-disabled-tooltip="permissionDeniedMessage"
|
||||
@dismiss="onContentErrorDismiss"
|
||||
@retry="emit('content-retry')"
|
||||
@dismiss="onInstallationDismiss"
|
||||
@retry="emit('installation-retry')"
|
||||
/>
|
||||
<UploadAdmonition
|
||||
v-else-if="item.kind === 'upload'"
|
||||
|
||||
@@ -13,20 +13,12 @@ export type PowerAction = 'Start' | 'Stop' | 'Restart' | 'Kill'
|
||||
export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
|
||||
const { formatMessage } = useVIntl()
|
||||
const client = injectModrinthClient()
|
||||
const { serverId, server, powerState, isSyncingContent, busyReasons } =
|
||||
injectModrinthServerContext()
|
||||
const { serverId, powerState, busyReasons } = injectModrinthServerContext()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { canUsePowerActions, permissionDeniedMessage } = useServerPermissions()
|
||||
|
||||
const isInstalling = computed(
|
||||
() =>
|
||||
server.value.status === 'installing' ||
|
||||
isSyncingContent.value ||
|
||||
busyReasons.value.some(
|
||||
(r) =>
|
||||
r.reason.id === 'servers.busy.installing' ||
|
||||
r.reason.id === 'servers.busy.syncing-content',
|
||||
),
|
||||
const isInstalling = computed(() =>
|
||||
busyReasons.value.some((reason) => reason.reason.id === 'servers.busy.installing'),
|
||||
)
|
||||
const isRunning = computed(() => powerState.value === 'running')
|
||||
const isStopping = computed(() => powerState.value === 'stopping')
|
||||
|
||||
@@ -14,7 +14,9 @@ export * from './scroll-indicator'
|
||||
export * from './server-backup'
|
||||
export * from './server-backups-queue'
|
||||
export * from './server-console'
|
||||
export * from './server-context-runtime'
|
||||
export * from './server-manage-core-runtime'
|
||||
export * from './server-panel-sync'
|
||||
export * from './server-permissions'
|
||||
export { applyEarsMod, removeEarsMod } from './skin-rendering/use-ears-mod-features'
|
||||
export * from './sticky-observer'
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
import type { AbstractModrinthClient, Archon } from '@modrinth/api-client'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { injectModrinthClient } from '../providers'
|
||||
|
||||
type ReadableRef<T> = Ref<T> | ComputedRef<T>
|
||||
type RuntimeUnsubscriber = () => void
|
||||
|
||||
type RuntimeReadyWaiter = {
|
||||
resolve: () => void
|
||||
reject: (error: Error) => void
|
||||
timeout: ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
type ServerContextRuntime = {
|
||||
client: AbstractModrinthClient
|
||||
serverId: string
|
||||
leases: number
|
||||
socketLeases: number
|
||||
syncLeases: number
|
||||
releaseTimer: ReturnType<typeof setTimeout> | null
|
||||
socketReleaseTimer: ReturnType<typeof setTimeout> | null
|
||||
syncReleaseTimer: ReturnType<typeof setTimeout> | null
|
||||
connectPromise: Promise<void> | null
|
||||
socketUnsubscribers: RuntimeUnsubscriber[]
|
||||
installProgressItems: Ref<Archon.Websocket.v0.InstallProgressItem[]>
|
||||
isSocketAuthenticated: Ref<boolean>
|
||||
isSocketAuthIncorrect: Ref<boolean>
|
||||
hasAuthoritativeInstallProgress: Ref<boolean>
|
||||
readyWaiters: Set<RuntimeReadyWaiter>
|
||||
destroyed: boolean
|
||||
}
|
||||
|
||||
export type ServerContextRuntimeLease = {
|
||||
serverId: string
|
||||
installProgressItems: Ref<Archon.Websocket.v0.InstallProgressItem[]>
|
||||
isSocketAuthenticated: Ref<boolean>
|
||||
isSocketAuthIncorrect: Ref<boolean>
|
||||
hasAuthoritativeInstallProgress: Ref<boolean>
|
||||
waitUntilReady: () => Promise<void>
|
||||
release: () => void
|
||||
}
|
||||
|
||||
type RetainServerContextRuntimeOptions = {
|
||||
connect?: boolean
|
||||
socket?: boolean
|
||||
sync?: boolean
|
||||
}
|
||||
|
||||
const runtimeReleaseDelay = 1000
|
||||
const authoritativeReadinessTimeout = 30000
|
||||
const runtimesByClient = new WeakMap<AbstractModrinthClient, Map<string, ServerContextRuntime>>()
|
||||
|
||||
function getClientRuntimes(client: AbstractModrinthClient) {
|
||||
let runtimes = runtimesByClient.get(client)
|
||||
if (!runtimes) {
|
||||
runtimes = new Map()
|
||||
runtimesByClient.set(client, runtimes)
|
||||
}
|
||||
return runtimes
|
||||
}
|
||||
|
||||
function isRuntimeReady(runtime: ServerContextRuntime) {
|
||||
return runtime.isSocketAuthenticated.value && runtime.hasAuthoritativeInstallProgress.value
|
||||
}
|
||||
|
||||
function resolveReadyWaiters(runtime: ServerContextRuntime) {
|
||||
if (!isRuntimeReady(runtime)) return
|
||||
|
||||
for (const waiter of runtime.readyWaiters) {
|
||||
clearTimeout(waiter.timeout)
|
||||
waiter.resolve()
|
||||
}
|
||||
runtime.readyWaiters.clear()
|
||||
}
|
||||
|
||||
function createServerContextRuntime(
|
||||
client: AbstractModrinthClient,
|
||||
serverId: string,
|
||||
): ServerContextRuntime {
|
||||
const runtime: ServerContextRuntime = {
|
||||
client,
|
||||
serverId,
|
||||
leases: 0,
|
||||
socketLeases: 0,
|
||||
syncLeases: 0,
|
||||
releaseTimer: null,
|
||||
socketReleaseTimer: null,
|
||||
syncReleaseTimer: null,
|
||||
connectPromise: null,
|
||||
socketUnsubscribers: [],
|
||||
installProgressItems: ref([]),
|
||||
isSocketAuthenticated: ref(false),
|
||||
isSocketAuthIncorrect: ref(false),
|
||||
hasAuthoritativeInstallProgress: ref(false),
|
||||
readyWaiters: new Set(),
|
||||
destroyed: false,
|
||||
}
|
||||
|
||||
return runtime
|
||||
}
|
||||
|
||||
function attachRuntimeSocketListeners(runtime: ServerContextRuntime) {
|
||||
if (runtime.socketUnsubscribers.length > 0) return
|
||||
|
||||
runtime.socketUnsubscribers = [
|
||||
runtime.client.archon.sockets.on(runtime.serverId, 'auth-ok', () => {
|
||||
runtime.isSocketAuthenticated.value = true
|
||||
runtime.isSocketAuthIncorrect.value = false
|
||||
runtime.hasAuthoritativeInstallProgress.value = false
|
||||
}),
|
||||
runtime.client.archon.sockets.on(runtime.serverId, 'auth-incorrect', () => {
|
||||
runtime.isSocketAuthenticated.value = false
|
||||
runtime.isSocketAuthIncorrect.value = true
|
||||
runtime.hasAuthoritativeInstallProgress.value = false
|
||||
}),
|
||||
runtime.client.archon.sockets.on(runtime.serverId, 'install-progress', (event) => {
|
||||
runtime.installProgressItems.value = event.items
|
||||
runtime.hasAuthoritativeInstallProgress.value = true
|
||||
resolveReadyWaiters(runtime)
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
function disconnectRuntimeSocket(runtime: ServerContextRuntime) {
|
||||
for (const unsubscribe of runtime.socketUnsubscribers) unsubscribe()
|
||||
runtime.socketUnsubscribers = []
|
||||
runtime.client.archon.sockets.disconnect(runtime.serverId)
|
||||
runtime.connectPromise = null
|
||||
runtime.isSocketAuthenticated.value = false
|
||||
runtime.isSocketAuthIncorrect.value = false
|
||||
runtime.hasAuthoritativeInstallProgress.value = false
|
||||
for (const waiter of runtime.readyWaiters) {
|
||||
clearTimeout(waiter.timeout)
|
||||
waiter.reject(new Error(`Node socket for server ${runtime.serverId} was released`))
|
||||
}
|
||||
runtime.readyWaiters.clear()
|
||||
}
|
||||
|
||||
function disconnectRuntimeSync(runtime: ServerContextRuntime) {
|
||||
runtime.client.archon.sync.disconnect(runtime.serverId)
|
||||
}
|
||||
|
||||
async function ensureRuntimeConnections(
|
||||
runtime: ServerContextRuntime,
|
||||
options: RetainServerContextRuntimeOptions = {},
|
||||
) {
|
||||
if (runtime.destroyed) {
|
||||
throw new Error(`Server context runtime for ${runtime.serverId} has been released`)
|
||||
}
|
||||
|
||||
const shouldConnectSocket = options.socket !== false
|
||||
const shouldConnectSync = options.sync !== false
|
||||
const socketStatus = runtime.client.archon.sockets.getStatus(runtime.serverId)
|
||||
if (shouldConnectSocket && !socketStatus?.connected) {
|
||||
attachRuntimeSocketListeners(runtime)
|
||||
runtime.isSocketAuthenticated.value = false
|
||||
runtime.hasAuthoritativeInstallProgress.value = false
|
||||
}
|
||||
|
||||
if (shouldConnectSync) {
|
||||
void runtime.client.archon.sync
|
||||
.safeConnectServer(runtime.serverId, { intent: 'all' })
|
||||
.catch((error) => {
|
||||
console.warn(
|
||||
`[server-context-runtime] Failed to connect sync stream for ${runtime.serverId}:`,
|
||||
error,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldConnectSocket && !runtime.connectPromise) {
|
||||
const connectPromise = runtime.client.archon.sockets
|
||||
.safeConnect(runtime.serverId)
|
||||
.then(() => {
|
||||
runtime.isSocketAuthenticated.value = true
|
||||
})
|
||||
.finally(() => {
|
||||
if (runtime.connectPromise === connectPromise) {
|
||||
runtime.connectPromise = null
|
||||
}
|
||||
})
|
||||
runtime.connectPromise = connectPromise
|
||||
}
|
||||
|
||||
if (runtime.connectPromise) await runtime.connectPromise
|
||||
}
|
||||
|
||||
async function waitUntilRuntimeReady(runtime: ServerContextRuntime) {
|
||||
await ensureRuntimeConnections(runtime)
|
||||
if (isRuntimeReady(runtime)) return
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const waiter: RuntimeReadyWaiter = {
|
||||
resolve,
|
||||
reject,
|
||||
timeout: setTimeout(() => {
|
||||
runtime.readyWaiters.delete(waiter)
|
||||
reject(
|
||||
new Error(
|
||||
`Timed out waiting for authoritative install progress for server ${runtime.serverId}`,
|
||||
),
|
||||
)
|
||||
}, authoritativeReadinessTimeout),
|
||||
}
|
||||
runtime.readyWaiters.add(waiter)
|
||||
resolveReadyWaiters(runtime)
|
||||
})
|
||||
}
|
||||
|
||||
function destroyRuntime(runtime: ServerContextRuntime) {
|
||||
if (runtime.destroyed || runtime.leases > 0) return
|
||||
runtime.destroyed = true
|
||||
|
||||
if (runtime.socketReleaseTimer) clearTimeout(runtime.socketReleaseTimer)
|
||||
if (runtime.syncReleaseTimer) clearTimeout(runtime.syncReleaseTimer)
|
||||
disconnectRuntimeSocket(runtime)
|
||||
disconnectRuntimeSync(runtime)
|
||||
|
||||
getClientRuntimes(runtime.client).delete(runtime.serverId)
|
||||
}
|
||||
|
||||
export function retainServerContextRuntime(
|
||||
client: AbstractModrinthClient,
|
||||
serverId: string,
|
||||
options: RetainServerContextRuntimeOptions = {},
|
||||
): ServerContextRuntimeLease {
|
||||
const runtimes = getClientRuntimes(client)
|
||||
let runtime = runtimes.get(serverId)
|
||||
if (!runtime) {
|
||||
runtime = createServerContextRuntime(client, serverId)
|
||||
runtimes.set(serverId, runtime)
|
||||
}
|
||||
|
||||
if (runtime.releaseTimer) {
|
||||
clearTimeout(runtime.releaseTimer)
|
||||
runtime.releaseTimer = null
|
||||
}
|
||||
const retainSocket = options.socket !== false
|
||||
const retainSync = options.sync !== false
|
||||
if (retainSocket) {
|
||||
if (runtime.socketReleaseTimer) {
|
||||
clearTimeout(runtime.socketReleaseTimer)
|
||||
runtime.socketReleaseTimer = null
|
||||
}
|
||||
attachRuntimeSocketListeners(runtime)
|
||||
runtime.socketLeases += 1
|
||||
}
|
||||
if (retainSync) {
|
||||
if (runtime.syncReleaseTimer) {
|
||||
clearTimeout(runtime.syncReleaseTimer)
|
||||
runtime.syncReleaseTimer = null
|
||||
}
|
||||
runtime.syncLeases += 1
|
||||
}
|
||||
runtime.leases += 1
|
||||
if (options.connect !== false) {
|
||||
void ensureRuntimeConnections(runtime, options).catch((error) => {
|
||||
if (runtime && runtime.leases > 0) {
|
||||
console.warn(
|
||||
`[server-context-runtime] Failed to connect node socket for ${serverId}:`,
|
||||
error,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let released = false
|
||||
return {
|
||||
serverId,
|
||||
installProgressItems: runtime.installProgressItems,
|
||||
isSocketAuthenticated: runtime.isSocketAuthenticated,
|
||||
isSocketAuthIncorrect: runtime.isSocketAuthIncorrect,
|
||||
hasAuthoritativeInstallProgress: runtime.hasAuthoritativeInstallProgress,
|
||||
waitUntilReady: () => waitUntilRuntimeReady(runtime),
|
||||
release: () => {
|
||||
if (released) return
|
||||
released = true
|
||||
runtime.leases = Math.max(0, runtime.leases - 1)
|
||||
if (retainSocket) {
|
||||
runtime.socketLeases = Math.max(0, runtime.socketLeases - 1)
|
||||
}
|
||||
if (retainSync) {
|
||||
runtime.syncLeases = Math.max(0, runtime.syncLeases - 1)
|
||||
}
|
||||
|
||||
if (runtime.leases === 0) {
|
||||
if (runtime.socketReleaseTimer) clearTimeout(runtime.socketReleaseTimer)
|
||||
if (runtime.syncReleaseTimer) clearTimeout(runtime.syncReleaseTimer)
|
||||
runtime.socketReleaseTimer = null
|
||||
runtime.syncReleaseTimer = null
|
||||
runtime.releaseTimer = setTimeout(() => {
|
||||
runtime.releaseTimer = null
|
||||
destroyRuntime(runtime)
|
||||
}, runtimeReleaseDelay)
|
||||
return
|
||||
}
|
||||
|
||||
if (retainSocket && runtime.socketLeases === 0) {
|
||||
runtime.socketReleaseTimer = setTimeout(() => {
|
||||
runtime.socketReleaseTimer = null
|
||||
if (runtime.socketLeases === 0) disconnectRuntimeSocket(runtime)
|
||||
}, runtimeReleaseDelay)
|
||||
}
|
||||
if (retainSync && runtime.syncLeases === 0) {
|
||||
runtime.syncReleaseTimer = setTimeout(() => {
|
||||
runtime.syncReleaseTimer = null
|
||||
if (runtime.syncLeases === 0) disconnectRuntimeSync(runtime)
|
||||
}, runtimeReleaseDelay)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function useServerContextRuntime(serverId: ReadableRef<string | null>) {
|
||||
const client = injectModrinthClient()
|
||||
let lease: ServerContextRuntimeLease | null = null
|
||||
|
||||
const stop = watch(
|
||||
() => serverId.value,
|
||||
(nextServerId) => {
|
||||
lease?.release()
|
||||
lease = null
|
||||
|
||||
if (typeof window !== 'undefined' && nextServerId) {
|
||||
lease = retainServerContextRuntime(client, nextServerId)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
stop()
|
||||
lease?.release()
|
||||
lease = null
|
||||
})
|
||||
}
|
||||
|
||||
export async function waitForServerContextRuntimeReady(
|
||||
client: AbstractModrinthClient,
|
||||
serverId: string,
|
||||
) {
|
||||
const lease = retainServerContextRuntime(client, serverId)
|
||||
try {
|
||||
await lease.waitUntilReady()
|
||||
} finally {
|
||||
lease.release()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
import type { Archon } from '@modrinth/api-client'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
type ReadableRef<T> = Ref<T> | ComputedRef<T>
|
||||
|
||||
export type ServerInstallationKey =
|
||||
| Exclude<Archon.Websocket.v0.InstallProgressKey, { type: 'file' }>
|
||||
| { type: 'unknown' }
|
||||
|
||||
export type ServerInstallationState = {
|
||||
id: string
|
||||
key: ServerInstallationKey
|
||||
status: 'pending' | 'installing' | 'complete' | 'failed'
|
||||
progress: number | null
|
||||
error: string | null
|
||||
source: 'optimistic' | 'websocket' | 'server'
|
||||
}
|
||||
|
||||
type OptimisticInstallation = {
|
||||
id: string
|
||||
key: ServerInstallationKey
|
||||
startRevision: number
|
||||
}
|
||||
|
||||
type UseServerInstallationTrackerOptions = {
|
||||
worldId: ReadableRef<string | null>
|
||||
server: ReadableRef<Archon.Servers.v0.Server | null | undefined>
|
||||
content?: ReadableRef<Archon.Content.v1.Addons | null | undefined>
|
||||
}
|
||||
|
||||
type ServerInstallationPlatform = Extract<ServerInstallationKey, { type: 'platform' }>['platform']
|
||||
|
||||
function installationKeyId(key: ServerInstallationKey) {
|
||||
switch (key.type) {
|
||||
case 'platform':
|
||||
return `platform:${key.platform}:${key.platform_version}:${key.game_version}`
|
||||
case 'modrinth_modpack':
|
||||
return `modrinth-modpack:${key.project_id}:${key.version_id}`
|
||||
case 'local_modpack':
|
||||
return `local-modpack:${key.filename}`
|
||||
case 'unknown':
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
function itemStatus(
|
||||
item: Archon.Websocket.v0.InstallProgressItem,
|
||||
): ServerInstallationState['status'] {
|
||||
if (item.error != null) return 'failed'
|
||||
if (item.progress === 100) return 'complete'
|
||||
return 'installing'
|
||||
}
|
||||
|
||||
function contentPlatform(modloader: string | null): ServerInstallationPlatform | null {
|
||||
const platform = modloader === 'neo_forge' ? 'neoforge' : modloader
|
||||
switch (platform) {
|
||||
case 'forge':
|
||||
case 'neoforge':
|
||||
case 'fabric':
|
||||
case 'quilt':
|
||||
case 'paper':
|
||||
case 'purpur':
|
||||
case 'vanilla':
|
||||
return platform
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function contentInstallationKey(content: Archon.Content.v1.Addons): ServerInstallationKey {
|
||||
if (content.installing === 'modpack' || (content.error && content.modpack)) {
|
||||
const spec = content.modpack?.spec
|
||||
if (spec?.platform === 'modrinth') {
|
||||
return {
|
||||
type: 'modrinth_modpack',
|
||||
project_id: spec.project_id,
|
||||
version_id: spec.version_id,
|
||||
}
|
||||
}
|
||||
if (spec?.platform === 'local_file') {
|
||||
return {
|
||||
type: 'local_modpack',
|
||||
filename: spec.filename,
|
||||
}
|
||||
}
|
||||
return { type: 'unknown' }
|
||||
}
|
||||
|
||||
const platform = contentPlatform(content.modloader)
|
||||
if (platform && content.game_version) {
|
||||
return {
|
||||
type: 'platform',
|
||||
platform,
|
||||
platform_version: content.modloader_version ?? '',
|
||||
game_version: content.game_version,
|
||||
}
|
||||
}
|
||||
|
||||
return { type: 'unknown' }
|
||||
}
|
||||
|
||||
function addonFailureId(addon: Archon.Content.v1.Addon) {
|
||||
return `addon:${addon.id}:${addon.filename}`
|
||||
}
|
||||
|
||||
function addonFailureError(addon: Archon.Content.v1.Addon) {
|
||||
return typeof addon.status === 'object' ? addon.status.failed.error : null
|
||||
}
|
||||
|
||||
export function useServerInstallationTracker(options: UseServerInstallationTrackerOptions) {
|
||||
const installProgressItems = ref<Archon.Websocket.v0.InstallProgressItem[]>([])
|
||||
const optimisticInstallation = ref<OptimisticInstallation | null>(null)
|
||||
const receivedProgressSnapshot = ref(false)
|
||||
const snapshotRevision = ref(0)
|
||||
const seenActiveIds = ref(new Set<string>())
|
||||
const dismissedIds = ref(new Set<string>())
|
||||
let unknownInstallationId = 0
|
||||
|
||||
const currentWorldItems = computed(() =>
|
||||
installProgressItems.value.filter((item) => item.world_id === options.worldId.value),
|
||||
)
|
||||
|
||||
const websocketInstallation = computed<ServerInstallationState | null>(() => {
|
||||
const optimistic = optimisticInstallation.value
|
||||
const candidates = currentWorldItems.value.filter(
|
||||
(item) => item.key.type !== 'file' && !dismissedIds.value.has(installationKeyId(item.key)),
|
||||
)
|
||||
|
||||
for (const item of candidates) {
|
||||
if (item.key.type === 'file') continue
|
||||
const id = installationKeyId(item.key)
|
||||
const status = itemStatus(item)
|
||||
if (status === 'complete') {
|
||||
if (
|
||||
optimistic &&
|
||||
snapshotRevision.value <= optimistic.startRevision &&
|
||||
!seenActiveIds.value.has(id)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
!optimistic &&
|
||||
!seenActiveIds.value.has(id) &&
|
||||
options.server.value?.status !== 'installing'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
key: item.key,
|
||||
status,
|
||||
progress: item.progress,
|
||||
error: item.error,
|
||||
source: 'websocket',
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const persistedAddonFailure = computed<ServerInstallationState | null>(() => {
|
||||
const addon = options.content?.value?.addons?.find((addon) => {
|
||||
const id = addonFailureId(addon)
|
||||
return addonFailureError(addon) != null && !dismissedIds.value.has(id)
|
||||
})
|
||||
if (!addon) return null
|
||||
|
||||
return {
|
||||
id: addonFailureId(addon),
|
||||
key: { type: 'unknown' },
|
||||
status: 'failed',
|
||||
progress: null,
|
||||
error: addonFailureError(addon),
|
||||
source: 'server',
|
||||
}
|
||||
})
|
||||
|
||||
const persistedInstallation = computed<ServerInstallationState | null>(() => {
|
||||
const content = options.content?.value
|
||||
if (!content || (!content.installing && !content.error)) return null
|
||||
|
||||
const key = contentInstallationKey(content)
|
||||
const id = installationKeyId(key)
|
||||
if (dismissedIds.value.has(id)) return null
|
||||
|
||||
return {
|
||||
id,
|
||||
key,
|
||||
status: content.error ? 'failed' : 'installing',
|
||||
progress: null,
|
||||
error: content.error?.message ?? null,
|
||||
source: 'server',
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => options.content?.value,
|
||||
(content) => {
|
||||
if (!content) return
|
||||
if (content.error) {
|
||||
optimisticInstallation.value = null
|
||||
return
|
||||
}
|
||||
if (!content.installing) return
|
||||
const id = installationKeyId(contentInstallationKey(content))
|
||||
if (dismissedIds.value.has(id)) {
|
||||
dismissedIds.value = new Set([...dismissedIds.value].filter((item) => item !== id))
|
||||
}
|
||||
optimisticInstallation.value = null
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => options.content?.value?.addons,
|
||||
(addons) => {
|
||||
const currentFailureIds = new Set(
|
||||
(addons ?? []).filter((addon) => addonFailureError(addon) != null).map(addonFailureId),
|
||||
)
|
||||
const nextDismissedIds = new Set(
|
||||
[...dismissedIds.value].filter(
|
||||
(id) => !id.startsWith('addon:') || currentFailureIds.has(id),
|
||||
),
|
||||
)
|
||||
if (nextDismissedIds.size !== dismissedIds.value.size) {
|
||||
dismissedIds.value = nextDismissedIds
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const installation = computed<ServerInstallationState | null>(() => {
|
||||
if (websocketInstallation.value) return websocketInstallation.value
|
||||
|
||||
const optimistic = optimisticInstallation.value
|
||||
if (optimistic && !dismissedIds.value.has(optimistic.id)) {
|
||||
return {
|
||||
id: optimistic.id,
|
||||
key: optimistic.key,
|
||||
status: 'pending',
|
||||
progress: null,
|
||||
error: null,
|
||||
source: 'optimistic',
|
||||
}
|
||||
}
|
||||
|
||||
if (persistedInstallation.value) return persistedInstallation.value
|
||||
if (persistedAddonFailure.value) return persistedAddonFailure.value
|
||||
if (options.content?.value?.error) return null
|
||||
|
||||
if (options.server.value?.status !== 'installing' || receivedProgressSnapshot.value) return null
|
||||
|
||||
return {
|
||||
id: `unknown:${unknownInstallationId}`,
|
||||
key: { type: 'unknown' },
|
||||
status: 'installing',
|
||||
progress: null,
|
||||
error: null,
|
||||
source: 'server',
|
||||
}
|
||||
})
|
||||
|
||||
const isBlocking = computed(
|
||||
() => installation.value?.status === 'pending' || installation.value?.status === 'installing',
|
||||
)
|
||||
|
||||
function handleProgress(items: Archon.Websocket.v0.InstallProgressItem[]) {
|
||||
snapshotRevision.value += 1
|
||||
receivedProgressSnapshot.value = true
|
||||
installProgressItems.value = items
|
||||
|
||||
const optimistic = optimisticInstallation.value
|
||||
const isPostOptimisticSnapshot =
|
||||
optimistic !== null && snapshotRevision.value > optimistic.startRevision
|
||||
const nextSeenActiveIds = new Set(seenActiveIds.value)
|
||||
const nextDismissedIds = new Set(dismissedIds.value)
|
||||
let hasAuthoritativeInstallation = false
|
||||
for (const item of items) {
|
||||
if (item.world_id !== options.worldId.value || item.key.type === 'file') continue
|
||||
hasAuthoritativeInstallation = true
|
||||
const id = installationKeyId(item.key)
|
||||
if (isPostOptimisticSnapshot) {
|
||||
nextDismissedIds.delete(id)
|
||||
}
|
||||
if (item.error == null && item.progress != null && item.progress < 100) {
|
||||
nextSeenActiveIds.add(id)
|
||||
nextDismissedIds.delete(id)
|
||||
}
|
||||
}
|
||||
seenActiveIds.value = nextSeenActiveIds
|
||||
dismissedIds.value = nextDismissedIds
|
||||
if (hasAuthoritativeInstallation) {
|
||||
optimisticInstallation.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function begin(key: ServerInstallationKey) {
|
||||
const id =
|
||||
key.type === 'unknown' ? `unknown:${++unknownInstallationId}` : installationKeyId(key)
|
||||
const nextDismissedIds = new Set(dismissedIds.value)
|
||||
nextDismissedIds.delete(id)
|
||||
dismissedIds.value = nextDismissedIds
|
||||
optimisticInstallation.value = {
|
||||
id,
|
||||
key,
|
||||
startRevision: snapshotRevision.value,
|
||||
}
|
||||
}
|
||||
|
||||
function cancelOptimistic() {
|
||||
optimisticInstallation.value = null
|
||||
}
|
||||
|
||||
function dismiss(id: string) {
|
||||
dismissedIds.value = new Set([...dismissedIds.value, id])
|
||||
if (optimisticInstallation.value?.id === id) {
|
||||
optimisticInstallation.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
installProgressItems.value = []
|
||||
optimisticInstallation.value = null
|
||||
receivedProgressSnapshot.value = false
|
||||
snapshotRevision.value = 0
|
||||
seenActiveIds.value = new Set()
|
||||
dismissedIds.value = new Set()
|
||||
unknownInstallationId = 0
|
||||
}
|
||||
|
||||
return {
|
||||
begin,
|
||||
cancelOptimistic,
|
||||
dismiss,
|
||||
handleProgress,
|
||||
installation,
|
||||
installProgressItems,
|
||||
isBlocking,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,23 @@ import {
|
||||
type UploadState,
|
||||
} from '@modrinth/api-client'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import type { FileOperation } from '../layouts/shared/files-tab/types'
|
||||
import { injectModrinthClient, provideModrinthServerContext } from '../providers'
|
||||
import type { BusyReason, CancelUploadHandler, ServerStats } from '../providers/server-context'
|
||||
import { defineMessage } from './i18n'
|
||||
import { useModrinthServersConsole } from './server-console'
|
||||
import {
|
||||
retainServerContextRuntime,
|
||||
type ServerContextRuntimeLease,
|
||||
} from './server-context-runtime'
|
||||
import { useServerInstallationTracker } from './server-installation-tracker'
|
||||
|
||||
type ReadableRef<T> = Ref<T> | ComputedRef<T>
|
||||
type SocketUnsubscriber = () => void
|
||||
|
||||
type ConnectSocketOptions = {
|
||||
force?: boolean
|
||||
extraSubscriptions?: (targetServerId: string) => SocketUnsubscriber[]
|
||||
}
|
||||
|
||||
@@ -26,7 +30,7 @@ type UseServerManageCoreRuntimeOptions = {
|
||||
worldId: ReadableRef<string | null>
|
||||
server: ReadableRef<Archon.Servers.v0.Server | null | undefined>
|
||||
serverFull?: ReadableRef<Archon.Servers.v1.ServerFull | null | undefined>
|
||||
isSyncingContent: ReadableRef<boolean>
|
||||
content?: ReadableRef<Archon.Content.v1.Addons | null | undefined>
|
||||
extraBusyReasons?: ComputedRef<BusyReason[]>
|
||||
setDisconnectedOnAuthIncorrect?: boolean
|
||||
syncUptimeFromState?: boolean
|
||||
@@ -96,10 +100,25 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
const fsAuth = ref<{ url: string; token: string } | null>(null)
|
||||
const fsOps = ref<Archon.Websocket.v0.FilesystemOperation[]>([])
|
||||
const fsQueuedOps = ref<Archon.Websocket.v0.QueuedFilesystemOp[]>([])
|
||||
const {
|
||||
begin: beginInstallation,
|
||||
cancelOptimistic: cancelOptimisticInstallation,
|
||||
dismiss: dismissInstallation,
|
||||
handleProgress: handleInstallProgress,
|
||||
installation,
|
||||
installProgressItems,
|
||||
isBlocking: isInstallationBlocking,
|
||||
reset: resetInstallation,
|
||||
} = useServerInstallationTracker({
|
||||
worldId: options.worldId,
|
||||
server: options.server,
|
||||
content: options.content,
|
||||
})
|
||||
const connectedSocketServerId = ref<string | null>(null)
|
||||
const socketUnsubscribers = ref<SocketUnsubscriber[]>([])
|
||||
const cpuData = ref<number[]>([])
|
||||
const ramData = ref<number[]>([])
|
||||
let serverContextRuntimeLease: ServerContextRuntimeLease | null = null
|
||||
|
||||
let uptimeIntervalId: ReturnType<typeof setInterval> | null = null
|
||||
let staleStatsTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
@@ -107,7 +126,7 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
|
||||
const busyReasons = computed<BusyReason[]>(() => {
|
||||
const reasons: BusyReason[] = []
|
||||
if (options.server.value?.status === 'installing') {
|
||||
if (isInstallationBlocking.value) {
|
||||
reasons.push({
|
||||
reason: defineMessage({
|
||||
id: 'servers.busy.installing',
|
||||
@@ -115,14 +134,6 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
}),
|
||||
})
|
||||
}
|
||||
if (options.isSyncingContent.value) {
|
||||
reasons.push({
|
||||
reason: defineMessage({
|
||||
id: 'servers.busy.syncing-content',
|
||||
defaultMessage: 'Content sync in progress',
|
||||
}),
|
||||
})
|
||||
}
|
||||
if (options.extraBusyReasons) reasons.push(...options.extraBusyReasons.value)
|
||||
return reasons
|
||||
})
|
||||
@@ -265,20 +276,6 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
startUptimeTicker()
|
||||
}
|
||||
|
||||
const handleAuthIncorrect = () => {
|
||||
if (!shouldProcessEvent()) return
|
||||
isWsAuthIncorrect.value = true
|
||||
if (options.setDisconnectedOnAuthIncorrect) {
|
||||
isConnected.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAuthOk = () => {
|
||||
if (!shouldProcessEvent()) return
|
||||
isWsAuthIncorrect.value = false
|
||||
isConnected.value = true
|
||||
}
|
||||
|
||||
const clearSocketListeners = () => {
|
||||
for (const unsub of socketUnsubscribers.value) unsub()
|
||||
socketUnsubscribers.value = []
|
||||
@@ -288,10 +285,8 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
if (!targetServerId && !connectedSocketServerId.value) return
|
||||
|
||||
clearSocketListeners()
|
||||
|
||||
if (targetServerId) {
|
||||
client.archon.sockets.disconnect(targetServerId)
|
||||
}
|
||||
serverContextRuntimeLease?.release()
|
||||
serverContextRuntimeLease = null
|
||||
|
||||
stopUptimeTicker()
|
||||
clearStaleStatsTimers()
|
||||
@@ -301,6 +296,7 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
serverPowerState.value = 'stopped'
|
||||
powerStateDetails.value = undefined
|
||||
uptimeSeconds.value = 0
|
||||
resetInstallation()
|
||||
}
|
||||
|
||||
const connectSocket = async (
|
||||
@@ -317,14 +313,11 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
disconnectSocket(connectedSocketServerId.value ?? undefined)
|
||||
|
||||
try {
|
||||
const safeConnectOptions = connectOptions.force ? { force: true } : undefined
|
||||
await client.archon.sockets.safeConnect(targetServerId, safeConnectOptions)
|
||||
const runtimeLease = retainServerContextRuntime(client, targetServerId, {
|
||||
connect: false,
|
||||
})
|
||||
serverContextRuntimeLease = runtimeLease
|
||||
connectedSocketServerId.value = targetServerId
|
||||
isConnected.value = true
|
||||
isWsAuthIncorrect.value = false
|
||||
|
||||
modrinthServersConsole.clear()
|
||||
modrinthServersConsole.beginInitialLogHydration()
|
||||
|
||||
const baseSubscriptions: SocketUnsubscriber[] = [
|
||||
client.archon.sockets.on(targetServerId, 'log', handleLog),
|
||||
@@ -333,15 +326,45 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
client.archon.sockets.on(targetServerId, 'state', handleState),
|
||||
client.archon.sockets.on(targetServerId, 'power-state', handlePowerState),
|
||||
client.archon.sockets.on(targetServerId, 'uptime', handleUptime),
|
||||
client.archon.sockets.on(targetServerId, 'auth-incorrect', handleAuthIncorrect),
|
||||
client.archon.sockets.on(targetServerId, 'auth-ok', handleAuthOk),
|
||||
watch(
|
||||
runtimeLease.installProgressItems,
|
||||
(items) => {
|
||||
if (shouldProcessEvent()) handleInstallProgress(items)
|
||||
},
|
||||
{ immediate: true },
|
||||
),
|
||||
watch(
|
||||
runtimeLease.isSocketAuthenticated,
|
||||
(authenticated) => {
|
||||
if (!shouldProcessEvent()) return
|
||||
if (authenticated || options.setDisconnectedOnAuthIncorrect) {
|
||||
isConnected.value = authenticated
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
),
|
||||
watch(
|
||||
runtimeLease.isSocketAuthIncorrect,
|
||||
(authIncorrect) => {
|
||||
if (shouldProcessEvent()) isWsAuthIncorrect.value = authIncorrect
|
||||
},
|
||||
{ immediate: true },
|
||||
),
|
||||
]
|
||||
const extraSubscriptions = connectOptions.extraSubscriptions?.(targetServerId) ?? []
|
||||
socketUnsubscribers.value = [...baseSubscriptions, ...extraSubscriptions]
|
||||
|
||||
modrinthServersConsole.clear()
|
||||
modrinthServersConsole.beginInitialLogHydration()
|
||||
|
||||
await runtimeLease.waitUntilReady()
|
||||
isConnected.value = true
|
||||
isWsAuthIncorrect.value = false
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('[hosting/manage] Failed to connect server socket:', error)
|
||||
isConnected.value = false
|
||||
disconnectSocket(targetServerId)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -402,7 +425,11 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
isServerRunning,
|
||||
stats,
|
||||
uptimeSeconds,
|
||||
isSyncingContent: options.isSyncingContent as Ref<boolean>,
|
||||
installProgressItems,
|
||||
installation,
|
||||
beginInstallation,
|
||||
cancelOptimisticInstallation,
|
||||
dismissInstallation,
|
||||
busyReasons,
|
||||
fsAuth,
|
||||
fsOps,
|
||||
@@ -423,20 +450,25 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
||||
|
||||
return {
|
||||
activeOperations,
|
||||
beginInstallation,
|
||||
busyReasons,
|
||||
cancelUpload,
|
||||
cancelOptimisticInstallation,
|
||||
cleanupCoreRuntime,
|
||||
connectSocket,
|
||||
connectedSocketServerId,
|
||||
cpuData,
|
||||
disconnectSocket,
|
||||
dismissInstallation,
|
||||
dismissOperation,
|
||||
fsAuth,
|
||||
fsOps,
|
||||
fsQueuedOps,
|
||||
installation,
|
||||
isConnected,
|
||||
isServerRunning,
|
||||
isWsAuthIncorrect,
|
||||
installProgressItems,
|
||||
powerStateDetails,
|
||||
ramData,
|
||||
refreshFsAuth,
|
||||
|
||||
@@ -5,11 +5,16 @@ import { onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
import { injectModrinthClient } from '#ui/providers'
|
||||
|
||||
import {
|
||||
retainServerContextRuntime,
|
||||
type ServerContextRuntimeLease,
|
||||
} from './server-context-runtime'
|
||||
|
||||
type ReadableRef<T> = Ref<T> | ComputedRef<T>
|
||||
type SyncUnsubscriber = () => void
|
||||
|
||||
type UseServerPanelSyncOptions = {
|
||||
serverId: ReadableRef<string>
|
||||
serverId: ReadableRef<string | null>
|
||||
worldId: ReadableRef<string | null>
|
||||
}
|
||||
|
||||
@@ -20,6 +25,7 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
let activeServerId: string | null = null
|
||||
let runtimeLease: ServerContextRuntimeLease | null = null
|
||||
let unsubscribers: SyncUnsubscriber[] = []
|
||||
let mounted = false
|
||||
let actionLogInvalidateTimer: ReturnType<typeof setTimeout> | null = null
|
||||
@@ -27,6 +33,8 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
||||
const legacyServerDetailKey = (serverId: string) => ['servers', 'detail', serverId] as const
|
||||
const serverV1DetailKey = (serverId: string) => ['servers', 'v1', 'detail', serverId] as const
|
||||
const contentListKey = (serverId: string) => ['content', 'list', 'v1', serverId] as const
|
||||
const modpackContentListKey = (serverId: string) =>
|
||||
['content', 'list', 'v1', serverId, 'modpack'] as const
|
||||
const actionLogBaseKey = (serverId: string) =>
|
||||
['servers', 'action-log', 'v1', 'infinite', serverId] as const
|
||||
|
||||
@@ -43,12 +51,9 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
||||
unsubscribers = [
|
||||
client.archon.sync.onAny(targetServerId, (event) => handleSyncEvent(targetServerId, event)),
|
||||
]
|
||||
|
||||
void client.archon.sync.safeConnectServer(targetServerId, { intent: 'all' }).catch((error) => {
|
||||
console.warn(
|
||||
`[server-panel-sync] Failed to connect sync stream for ${targetServerId}:`,
|
||||
error,
|
||||
)
|
||||
runtimeLease = retainServerContextRuntime(client, targetServerId, {
|
||||
socket: false,
|
||||
sync: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,10 +66,9 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
||||
for (const unsubscribe of unsubscribers) unsubscribe()
|
||||
unsubscribers = []
|
||||
|
||||
if (activeServerId) {
|
||||
client.archon.sync.disconnect(activeServerId)
|
||||
activeServerId = null
|
||||
}
|
||||
runtimeLease?.release()
|
||||
runtimeLease = null
|
||||
activeServerId = null
|
||||
}
|
||||
|
||||
function handleSyncEvent(serverId: string, event: Archon.Sync.v1.SyncEvent) {
|
||||
@@ -111,6 +115,9 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
||||
case 'world.content.base.update':
|
||||
handleWorldContentBaseUpdate(serverId, event)
|
||||
break
|
||||
case 'world.content.update':
|
||||
handleWorldContentUpdate(serverId, event)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +223,106 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
||||
void queryClient.invalidateQueries({ queryKey: serverV1DetailKey(serverId) })
|
||||
}
|
||||
|
||||
function handleWorldContentUpdate(
|
||||
serverId: string,
|
||||
event: Archon.Sync.v1.WorldContentUpdateEvent,
|
||||
) {
|
||||
if (event.world_id !== options.worldId.value) {
|
||||
void invalidateContentAndServerDetails(serverId)
|
||||
return
|
||||
}
|
||||
|
||||
const content = worldContentUpdateToAddons(event)
|
||||
queryClient.setQueryData<Archon.Content.v1.Addons>(contentListKey(serverId), {
|
||||
...content,
|
||||
addons: content.addons?.filter((addon) => !addon.from_modpack) ?? null,
|
||||
})
|
||||
queryClient.setQueryData<Archon.Content.v1.Addons>(modpackContentListKey(serverId), {
|
||||
...content,
|
||||
addons: content.addons?.filter((addon) => addon.from_modpack) ?? null,
|
||||
})
|
||||
void queryClient.invalidateQueries({ queryKey: serverV1DetailKey(serverId) })
|
||||
}
|
||||
|
||||
function worldContentUpdateToAddons(
|
||||
event: Archon.Sync.v1.WorldContentUpdateEvent,
|
||||
): Archon.Content.v1.Addons {
|
||||
return {
|
||||
modloader: event.platform_data?.platform ?? null,
|
||||
modloader_version: event.platform_data?.platform_version ?? null,
|
||||
game_version: event.platform_data?.game_version ?? null,
|
||||
modpack: worldContentModpackToModpackFields(event.linked_modpack),
|
||||
installing: event.installing,
|
||||
error: event.error,
|
||||
addons: event.content.map(worldContentItemToAddon),
|
||||
}
|
||||
}
|
||||
|
||||
function worldContentModpackToModpackFields(
|
||||
modpack: Archon.Sync.v1.WorldContentModpack | null,
|
||||
): Archon.Content.v1.ModpackFields | null {
|
||||
if (!modpack || modpack.spec === 'CurseForge') return null
|
||||
|
||||
const spec: Archon.Content.v1.ModpackSpec =
|
||||
'Modrinth' in modpack.spec
|
||||
? {
|
||||
platform: 'modrinth',
|
||||
project_id: modpack.spec.Modrinth.project_id,
|
||||
version_id: modpack.spec.Modrinth.version_id,
|
||||
}
|
||||
: {
|
||||
platform: 'local_file',
|
||||
filename: modpack.spec.LocalMrPackFile.path,
|
||||
name: modpack.spec.LocalMrPackFile.name,
|
||||
description: modpack.spec.LocalMrPackFile.description,
|
||||
}
|
||||
|
||||
return {
|
||||
spec,
|
||||
has_update: modpack.has_update,
|
||||
title: modpack.title,
|
||||
description: modpack.description,
|
||||
icon_url: modpack.icon_url,
|
||||
owner: modpack.owner,
|
||||
version_number: modpack.version_number,
|
||||
date_published: modpack.date_published,
|
||||
downloads: modpack.downloads,
|
||||
followers: modpack.followers,
|
||||
}
|
||||
}
|
||||
|
||||
function worldContentItemToAddon(item: Archon.Sync.v1.WorldContentItem): Archon.Content.v1.Addon {
|
||||
return {
|
||||
id: item.version?.id ?? item.version_id ?? item.file_sha1 ?? item.filename,
|
||||
filename: item.filename,
|
||||
filesize: item.filesize ?? 0,
|
||||
btime: item.btime,
|
||||
disabled: item.filename.endsWith('.disabled'),
|
||||
kind: parentDirectoryToAddonKind(item.parent_directory),
|
||||
from_modpack: item.from_modpack,
|
||||
status: item.status,
|
||||
pack_client_retained: item.pack_client_retained,
|
||||
pack_client_depends: item.pack_client_depends,
|
||||
has_update: item.has_update,
|
||||
name: item.name,
|
||||
project_id: item.project_id,
|
||||
version: item.version,
|
||||
owner: item.owner,
|
||||
icon_url: item.icon_url,
|
||||
}
|
||||
}
|
||||
|
||||
function parentDirectoryToAddonKind(parentDirectory: string): Archon.Content.v1.AddonKind {
|
||||
switch (parentDirectory) {
|
||||
case 'plugins':
|
||||
return 'plugin'
|
||||
case 'datapacks':
|
||||
return 'datapack'
|
||||
default:
|
||||
return 'mod'
|
||||
}
|
||||
}
|
||||
|
||||
function handleBackupEvent(serverId: string) {
|
||||
void queryClient.invalidateQueries({ queryKey: ['backups', 'queue', serverId] })
|
||||
void invalidateServerDetails(serverId)
|
||||
@@ -296,7 +403,9 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
||||
return {
|
||||
...current,
|
||||
...incoming,
|
||||
status: incoming.status ?? current.status,
|
||||
filesize: incoming.filesize || current.filesize,
|
||||
btime: incoming.btime ?? current.btime,
|
||||
name: incoming.name ?? current.name,
|
||||
owner: incoming.owner ?? current.owner,
|
||||
icon_url: incoming.icon_url ?? current.icon_url,
|
||||
@@ -318,7 +427,7 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
||||
|
||||
onMounted(() => {
|
||||
mounted = true
|
||||
connect(options.serverId.value)
|
||||
if (options.serverId.value) connect(options.serverId.value)
|
||||
})
|
||||
|
||||
watch(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3659,36 +3659,9 @@
|
||||
"servers.grant-access-modal.target.searching": {
|
||||
"defaultMessage": "Vyhledávání..."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Modrinth Hosting tuto verzi Minecraftu nebo loaderu zatím nepodporuje."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Instalování addonů..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Instalování modpacku..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Instalování platformy..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Připravujeme váš server"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Přidávání Javy..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Konfigurování serveru..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Stahování módů..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Organizování souborů..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Nastavování prostředí..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Už máte server?"
|
||||
},
|
||||
|
||||
@@ -1961,9 +1961,6 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Installering fejlede"
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Konfigurer server..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Har du allerede en server?"
|
||||
},
|
||||
|
||||
@@ -4559,9 +4559,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "Server wird installiert"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Inhalt wird synchronisiert"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Auch eine Freundschaftsanfrage senden"
|
||||
},
|
||||
@@ -4616,51 +4613,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Installation fehlgeschlagen"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Ein interner Fehler ist beim Installieren der Platform aufgetreten. Bitte versuch es später erneut."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "Der angegebene Loader oder Minecraft Version konnte nicht installiert werden. Sie ist womöglich ungültig oder nicht unterstützt."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "Das Modpack konnte nicht installiert werden. Es könnte womöglich beschädigt oder inkompatibel sein."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Diese Modpack-Version enthält keine hrunterladbare Datei. Es wurde womöglich inkorrekt erstellt."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Ein unerwarteter Fehler ist während der Installation aufgetreten."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Diese Version von Minecraft oder des Loaders werden aktuell noch nicht von Modrinth Hosting unterstützt."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Installiere Addons..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Installiere Modpack..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Installiere Platform..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Wir bereiten deinen Server vor"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Füge Java hinzu..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Konfiguriere Server..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Lade Mods herunter..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Organisiere Dateien..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Richte die Umgebung ein..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Du hast bereits einen Server?"
|
||||
},
|
||||
|
||||
@@ -4559,9 +4559,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "Server wird installiert"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Inhalt wird synchronisiert"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Auch eine Freundschaftsanfrage senden"
|
||||
},
|
||||
@@ -4616,51 +4613,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Installation fehlgeschlagen"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Während der Installation der Plattform ist ein interner Fehler aufgetreten. Bitte versuche es erneut."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "Die angegebene Loader- oder Minecraft-Version konnte nicht installiert werden. Sie ist möglicherweise ungültig oder wird nicht unterstützt."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "Das Modpack konnte nicht installiert werden. Es ist eventuell beschädigt oder nicht kompatibel."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Diese Modpack-Version enthält keine herunterladbare Datei. Es wurde eventuell falsch gepackt."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Während der Installation ist ein unerwarteter Fehler aufgetreten."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Diese Version von Minecraft oder des Loaders wird aktuell noch nicht von Modrinth Hosting unterstützt."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Addons werden installiert..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Modpack wird installiert..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Plattform wird installiert..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Wir bereiten deinen Server vor"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Java wird hinzugefügt..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Server wird konfiguriert..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Mods werden heruntergeladen..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Dateien werden organisiert..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Umgebung wird eingerichtet..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Du hast bereits einen Server?"
|
||||
},
|
||||
|
||||
@@ -4865,9 +4865,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "Server is installing"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Content sync in progress"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Also send a friend request"
|
||||
},
|
||||
@@ -4919,53 +4916,56 @@
|
||||
"servers.grant-access-modal.target.searching": {
|
||||
"defaultMessage": "Searching..."
|
||||
},
|
||||
"servers.installing-banner.description.applying": {
|
||||
"defaultMessage": "Applying your installation changes..."
|
||||
},
|
||||
"servers.installing-banner.description.controls": {
|
||||
"defaultMessage": "Server controls will unlock when installation finishes."
|
||||
},
|
||||
"servers.installing-banner.description.duration": {
|
||||
"defaultMessage": "This installation may take several minutes..."
|
||||
},
|
||||
"servers.installing-banner.description.preparing": {
|
||||
"defaultMessage": "Preparing your server..."
|
||||
},
|
||||
"servers.installing-banner.description.still-working": {
|
||||
"defaultMessage": "Still working—your installation is in progress..."
|
||||
},
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Installation failed"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "An internal error occurred while installing the platform. Please try again."
|
||||
"servers.installing-banner.error.header.local-modpack": {
|
||||
"defaultMessage": "Failed to install {filename}"
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "The specified loader or Minecraft version could not be installed. It may be invalid or unsupported."
|
||||
"servers.installing-banner.error.header.minecraft": {
|
||||
"defaultMessage": "Failed to install Minecraft {version}"
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "The modpack could not be installed. It may be corrupted or incompatible."
|
||||
"servers.installing-banner.error.header.modpack": {
|
||||
"defaultMessage": "Failed to install modpack"
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "This modpack version does not include a downloadable file. It may have been packaged incorrectly."
|
||||
"servers.installing-banner.error.header.platform": {
|
||||
"defaultMessage": "Failed to install {loader} {loaderVersion} for Minecraft {gameVersion}"
|
||||
},
|
||||
"servers.installing-banner.error.header.platform-without-version": {
|
||||
"defaultMessage": "Failed to install {loader} for Minecraft {gameVersion}"
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "An unexpected error occurred during installation."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "This version of Minecraft or loader is not yet supported by Modrinth Hosting."
|
||||
"servers.installing-banner.installing-local-modpack": {
|
||||
"defaultMessage": "Installing {filename}"
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Installing addons..."
|
||||
"servers.installing-banner.installing-minecraft": {
|
||||
"defaultMessage": "Installing Minecraft {version}"
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Installing modpack..."
|
||||
"servers.installing-banner.installing-modpack": {
|
||||
"defaultMessage": "Installing modpack"
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Installing platform..."
|
||||
"servers.installing-banner.installing-platform": {
|
||||
"defaultMessage": "Installing {loader} for Minecraft {version}"
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "We're preparing your server"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Adding Java..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Configuring server..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Downloading mods..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Organizing files..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Setting up environment..."
|
||||
"defaultMessage": "Preparing your server"
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Already have a server?"
|
||||
|
||||
@@ -4559,9 +4559,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "El servidor se está instalando"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Sincronización de contenido en curso"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Mandar solicitud de amistad también"
|
||||
},
|
||||
@@ -4616,51 +4613,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Fallo en la instalación"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Se ha producido un error interno durante la instalación de la plataforma. Por favor, inténtalo de nuevo."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "No se pudo instalar el loader o la versión de Minecraft especificados. Es posible que no sean válidos o que no sean compatibles."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "No se pudo instalar el modpack. Es posible que esté dañado o que no sea compatible."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Esta versión del modpack no incluye ningún archivo descargable. Es posible que se haya empaquetado incorrectamente."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Se produjo un error inesperado durante la instalación."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Esta versión de Minecraft o del loader aún no es compatible con Modrinth Hosting."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Instalando addons..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Instalando modpack..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Instalando plataforma..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Estamos preparando tu servidor"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Añadiendo java..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Configurando servidor..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Descargando mods..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Organizando archivos..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Preparando entorno..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "¿Ya tienes un servidor?"
|
||||
},
|
||||
|
||||
@@ -4556,9 +4556,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "El servidor se está instalando"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Sincronización de contenido en progreso"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Enviar también una solicitud de amistad"
|
||||
},
|
||||
@@ -4613,51 +4610,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Instalación fallida"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Se ha producido un error interno durante la instalación de la plataforma. Por favor, inténtalo de nuevo."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "No se pudo instalar el loader o la versión de Minecraft especificados. Es posible que no sean válidos o que no sean compatibles."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "No se pudo instalar el modpack. Es posible que esté dañado o que no sea compatible."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Esta versión del modpack no incluye ningún archivo descargable. Es posible que se haya empaquetado incorrectamente."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Se produjo un error inesperado durante la instalación."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Esta versión de Minecraft o del loader aún no es compatible con Modrinth Hosting."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Instalando addons..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Instalando modpack..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Instalando plataforma..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Estamos preparando tu servidor"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Añadiendo Java..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Configurando servidor..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Descargando mods..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Organizando archivos..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Preparando entorno..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "¿Ya tienes un servidor?"
|
||||
},
|
||||
|
||||
@@ -4553,9 +4553,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "Le serveur est en cours d'installation"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Synchronisation du contenu en cours"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Envoyer également une demande d'ami"
|
||||
},
|
||||
@@ -4610,51 +4607,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Impossible d'installer"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Une erreur interne est survenue lors de l'installation de la platforme. Veuillez réessayer plus tard."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "Le loader ou la version Minecraft spécifiée n'a pas pu être installée. Elle est peut-être invalide ou non prise en charge."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "Le modpack n'a pas pu être installé. Il est peut-être corrompu ou incompatible."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Cette version de modpack ne contient pas de fichier téléchargeable. Il a peut-être été empaqueté incorrectement."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Une erreur inattendue est survenue lors de l'installation."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Cette version de Minecraft ou ce loader n'est peut-être pas encore bien pris en charge par Modrinth Hosting."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Installation des add-ons..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Installation du modpack..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Installation de la platforme..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Nous préparons votre serveur"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Ajout de Java..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Configuration du serveur..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Téléchargement de mods..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Organisation des fichiers..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Mise en place de l'environnement..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Vous avez déjà un serveur ?"
|
||||
},
|
||||
|
||||
@@ -3893,39 +3893,9 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "A telepítés nem sikerült"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Egy internal hiba történt a telepítés közben. Kérlek probáld újra."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Ez a modcsomag-verzió nem tartalmaz letölthető fájlt. Lehet, hogy hibásan lett összeállítva."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Kiegészítők telepítése..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "A modcsomag telepítése..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "A platform telepítése..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Előkészítjük a szervered"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Java hozzáadása..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "A szerver beállítása..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Modok letöltése..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Fájlok rendezése..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Környezet beállítása..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Már van egy szervered?"
|
||||
},
|
||||
|
||||
@@ -4517,9 +4517,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "Installazione del server in corso"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Sincronizzazione dei contenuti in corso"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Invia anche una richiesta di amicizia"
|
||||
},
|
||||
@@ -4574,51 +4571,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Installazione fallita"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Si è verificato un errore durante l'installazione della piattaforma. Riprova più tardi."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "Questa versione di Minecraft o del loader non è potuta essere installata. Potrebbe essere non valida o non supportata."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "Questo pacchetto non è potuto essere installato. Potrebbe essere corrotto o incompatibile."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Questa versione del pacchetto non include un file scaricabile. Potrebbe essere stata malformata."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Si è verificato un errore durante l'installazione."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Questa versione di Minecraft o del loader non è ancora supportata da Modrinth Hosting."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Installando gli addon..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Installando il pacchetto..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Installando la piattaforma..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Stiamo preparando il tuo server"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Aggiungendo Java..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Configurando il server..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Scaricando le mod..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Organizzando i file..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Configurando l'ambiente..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Hai già un server?"
|
||||
},
|
||||
|
||||
@@ -4403,9 +4403,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "サーバーがインストール中です"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "コンテンツの同期処理中です"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "フレンドのリクエストも送信する"
|
||||
},
|
||||
@@ -4460,51 +4457,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "インストールが失敗しました"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "プラットフォームのインストール中、内部エラーが発生しました。もう一度お試しください。"
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "特定のModローダーMinecraftのバージョンをインストールできませんでした。いずれかが非対応または無効である可能性があります。"
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "このModパックをインストールすることができませんでした。Modが非対応または破損している可能性があります。"
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "このModパックのバージョンには、ダウンロード可能なファイルが含まれていません。パッケージ化が正しく行われていない可能性があります。"
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "インストール中に予期せぬエラーが発生しました。"
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "このMinecraftのバージョンまたはModローダーはModrinthホスティングでまだ対応されていません。"
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "アドオンをインストールしています…"
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Modパックをインストールしています…"
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "プラットフォームをインストールしています…"
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "あなたのサーバーを準備しています"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Javaを追加しています…"
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "サーバーの設定をしています…"
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Modをダウンロードしています…"
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "ファイルを整理しています…"
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "環境を整えています…"
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "既にサーバーをお持ちですか?"
|
||||
},
|
||||
|
||||
@@ -4544,9 +4544,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "서버 설치 중"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "콘텐츠 동기화 진행 중"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "친구 요청도 보내기"
|
||||
},
|
||||
@@ -4601,51 +4598,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "설치 실패"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "플랫폼 설치 중 내부 오류가 발생했습니다. 다시 시도해 주세요."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "지정한 로더 또는 마인크래프트 버전을 설치할 수 없습니다. 유효하지 않거나 지원되지 않는 것일 수 있습니다."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "모드팩을 설치할 수 없습니다. 파일이 손상되었거나 호환되지 않을 수 있습니다."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "이 모드팩 버전은 다운로드 가능한 파일을 포함되어 있지 않습니다. 패키징이 잘못되었을 수 있습니다."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "설치 중 알 수 없는 오류가 발생했습니다."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "이 미인크래프트 또는 로더 버전은 아직 Modrinth Hosting에서 지원되지 않습니다."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "애드온 설치 중..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "모드팩 설치 중..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "플랫폼 설치 중..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "서버를 준비하고 있습니다"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Java 추가 중..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "서버 구성 중..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "모드 다운로드 중..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "파일 준비 중..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "환경 설정 중..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "서버가 이미 있으신가요?"
|
||||
},
|
||||
|
||||
@@ -3848,9 +3848,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "Pelayan sedang dipasang"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Penyegerakan kandungan sedang dijalankan"
|
||||
},
|
||||
"servers.grant-access-modal.cancel": {
|
||||
"defaultMessage": "Batal"
|
||||
},
|
||||
@@ -3881,45 +3878,9 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Pemasangan gagal"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Sebuah ralat dalaman telah berlaku semasa memasang platform. Sila cuba lagi kemudian."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "Pemuat atau versi Minecraft yang dinyatakan tidak dapat dipasang. Ia mungkin tidak sah atau tidak disokong."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Versi pek mod ini tidak menyertakan fail yang boleh dimuat turun. Ia mungkin telah dibungkus secara salah."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Versi Minecraft atau pemuat ini belum disokong oleh Modrinth Hosting."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Sedang memasang tambahan..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Sedang memasang pek mod..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Sedang memasang platform..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Kami sedang menyediakan pelayan anda"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Sedang menambah Java..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Sedang mengkonfigurasikan pelayan..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Sedang memuat turun mod..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Sedang menyusun fail..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Sedang menyediakan persekitaran..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Sudah mempunyai pelayan?"
|
||||
},
|
||||
|
||||
@@ -4550,9 +4550,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "De server wordt geïnstalleerd"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Inhoud wordt gesynchroniseerd"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Ook een vriendschapsverzoek sturen"
|
||||
},
|
||||
@@ -4607,51 +4604,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Installeren is mislukt"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Er is een interne fout opgetreden tijdens het installeren van het platform. Probeer het nog eens."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "De opgegeven loader of Minecraft-versie kon niet worden geïnstalleerd. Deze is mogelijk ongeldig of wordt niet ondersteund."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "Het modpack kon niet worden geïnstalleerd. Het is mogelijk beschadigd of niet compatibel."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Deze versie van het modpack bevat geen downloadbaar bestand. Mogelijk is het pakket niet correct samengesteld."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Tijdens de installatie is er een onverwachte fout opgetreden."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Deze versie van Minecraft of loader wordt nog niet ondersteund door Modrinth Hosting."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Add-ons aan het installeren..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Modpack aan het installeren..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Platform aan het installeren..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "We zijn je server aan het klaarmaken"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Java toevoegen..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Server wordt geconfigureerd..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Mods worden gedownload..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Bestanden worden geordend..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Omgeving wordt opgezet..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Heb je al een server?"
|
||||
},
|
||||
|
||||
@@ -4526,9 +4526,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "Serwer jest instalowany"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Synchronizacja zawartości w toku"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Wyślij także zaproszenie do znajomych"
|
||||
},
|
||||
@@ -4583,51 +4580,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Instalacja nie powiodła się"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Wystąpił wewnętrzny błąd podczas instalowania platformy. Spróbuj ponownie później."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "Nie udało się zainstalować podanego loadera lub podanej wersji Minecraft. Mogą być niepoprawne lub niewspierane."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "Nie udało się zainstalować paczki modów. Może być zepsuta lub niekompatybilna."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Ta wersja paczki modów nie zawiera pliku do pobrania. Możliwe, że została niepoprawnie zapakowana."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Wystąpił nieoczekiwany błąd podczas instalacji."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Ta wersja Minecraft lub loader nie są jeszcze wspierane przez Modrinth Hosting."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Instalowanie dodatków..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Instalowanie paczki modów..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Instalowanie platformy..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Przygotowujemy Twój serwer"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Dodawanie Java..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Ustawianie serwera..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Pobieranie modów..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Organizowanie plików..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Ustawianie środowiska..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Posiadasz już serwer?"
|
||||
},
|
||||
|
||||
@@ -4559,9 +4559,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "O servidor está sendo instalado"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Sincronização de conteúdo em andamento"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Envie também um pedido de amizade"
|
||||
},
|
||||
@@ -4616,51 +4613,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "A instalação falhou"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Ocorreu um erro interno durante a instalação da plataforma. Tente novamente."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "Não foi possível instalar o carregador ou a versão do Minecraft especificada. Ela pode ser inválida ou não suportada."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "O pacote de mods não pôde ser instalado. Ele pode estar corrompido ou ser incompatível."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Esta versão do pacote de mods não inclui um arquivo para download. Ela pode ter sido empacotada incorretamente."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Um erro inesperado ocorreu durante a instalação"
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Esta versão do Minecraft ou do carregador ainda não é suportada pelo Modrinth Hosting."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Instalando complementos..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Instalando pacote de mods..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Instalando plataforma"
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Nós estamos preparando seu servidor"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Adicionando Java..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Configurando servidor..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Baixando mods..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Organizando arquivos..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Configurando ambiente..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Já possui um servidor?"
|
||||
},
|
||||
|
||||
@@ -4472,9 +4472,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "Сервер устанавливается"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Синхронизация контента в процессе"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Также отправить запрос в друзья"
|
||||
},
|
||||
@@ -4529,51 +4526,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Установка не удалась"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Во время установки платформы произошла внутренняя ошибка. Попробуйте ещё раз."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "Не удалось установить указанную версию загрузчика или Minecraft. Возможно, она недействительна или не поддерживается."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "Не удалось установить сборку модов. Возможно, она повреждена или несовместима."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Эта версия сборки не содержит загрузочного файла. Возможно, она была собрана неправильно."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Во время установки произошла непредвиденная ошибка."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Эта версия Minecraft или загрузчика пока не поддерживается Modrinth Hosting."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Установка дополнений..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Установка сборки..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Установка платформы..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Подготовка сервера"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Добавление Java..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Настройка сервера..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Скачивание модов..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Упорядочивание файлов..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Настройка среды..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Уже есть сервер?"
|
||||
},
|
||||
|
||||
@@ -4328,9 +4328,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "Server se instalira"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Sinhronizacija sadržaja u toku"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Pošalji i zahtev za prijateljstvo"
|
||||
},
|
||||
@@ -4385,51 +4382,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Neuspešna instalacija"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Došlo je do interne greške prilikom instaliranja platforme. Molimo te, pokušaj ponovo."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "Navedeni učitavač ili verzija Minecrafta se ne mogu instalirati. Moguće je da su nevažeći ili nepodržani."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "Modpak nije mogao biti instaliran. Možda je oštećen ili nekompatibilan."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Ova verzija modpaka ne uključuje datoteku za preuzimanje. Moguće je da je pogrešno upakovana."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Došlo je do neočekivane greške tokom instalacije."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Ova verzija Minecrafta ili učitavača još uvek nije podržana od strane Modrinth Hosting-a."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Instaliranje addona..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Instaliranje modpacka..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Instaliranje platforme..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Pripremamo tvoj server"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Dodavanje Jave..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Konfigurisanje servera..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Instaliranje modova..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Organiziranje datoteka..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Podešavanje okruženja..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Već imaš server?"
|
||||
},
|
||||
|
||||
@@ -4145,42 +4145,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Installering misslyckades"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Ett internt fel inträffade när plattformen installerades. Vänligen försök igen."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "Den angivna loader eller Minecraft-versionen kunde inte installeras. Den kan vara ogiltig eller stöds inte."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "Modpaketet kunde inte installeras. Det kanske är korrumperad eller inkompatibel."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Ett oväntat fel inträffade under installeringen."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Denna version av Minecraft eller loader stöds ännu inte av Modrinth Hosting."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Installerar tillägg..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Vi förbereder din server"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Lägger till Java..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Konfigurerar server..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Laddar ner moddar..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Organiserar filer..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Ställer in miljö..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Har du redan en server?"
|
||||
},
|
||||
|
||||
@@ -4379,9 +4379,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "Sunucu kuruluyor"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "İçerik eşlemesi sürüyor"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Ayrıca bir arkadaş isteği gönderin"
|
||||
},
|
||||
@@ -4436,51 +4433,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Kurulum başarısız oldu"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Platform yüklenirken dahili bir hata oluştu. Lütfen tekrar deneyin."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "Belirtilen yükleyici veya Minecraft sürümü kurulamazdı. Belki de geçersiz veya desteklenmeyen bir sürüm olabilir."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "Mod paketi yüklenemedi. Bozuk veya uyumsuz olabilir."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Bu mod paketi sürümü indirilebilir bir dosya içermez. Yanlış paketlenmiş olabilir."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Kurulum sırasında beklenmeyen bir hata oluştu."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Minecraft veya yükleyicinin bu sürümü henüz Modrinth Hosting tarafından desteklenmemektedir."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Eklentiler yükleniyor..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Mod paketi indiriliyor..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Platform yükleniyor..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Sunucunuzu hazırlıyoruz"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Java ekleniyor..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Sunucu yapılandırılıyor..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Modlar indiriliyor..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Dosyalar düzenleniyor..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Ortam hazırlanıyor..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Zaten sunucunuz var mı?"
|
||||
},
|
||||
|
||||
@@ -4514,9 +4514,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "Сервер установлюється"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Триває синхронізація вмісту"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "Також відправити запит у друзі"
|
||||
},
|
||||
@@ -4571,51 +4568,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Не вдалося встановити"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Під час установлення платформи сталася внутрішня помилка. Спробуйте ще раз."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "Не вдалося встановити вказаний завантажувач або версію Minecraft. Він може бути недійсним або не підтримуватися."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "Збірку не вдалося встановити. Вона може бути пошкоджена або несумісна."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Ця версія збірки не містить завантажуваних файлів. Можливо, вона була неправильно впакований."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Під час встановлення сталася неочікувана помилка."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Ця версія Minecraft або завантажувача ще не підтримується Modrinth Hosting."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Установлення доповнень…"
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Установлення збірки…"
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Установлення платформи…"
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Ми підготовлюємо ваш сервер"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Додання Java…"
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Налаштування сервера…"
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Завантаження модів…"
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Організування файлів…"
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Налаштування середовища…"
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Уже маєте сервер?"
|
||||
},
|
||||
|
||||
@@ -3881,57 +3881,15 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "Đang cài máy chủ"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "Đang đồng bộ nội dung"
|
||||
},
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "Tải thất bại"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "Đã xảy ra lỗi nội bộ trong quá trình cài đặt nền tảng. Thử lại."
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "Không thể tải loader hoặc phiên bản Minecraft đã chọn. Nó có thể không phù hợp hoặc không được hỗ trợ."
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "Modpack này không thể tải được. Nó có thể bị hỏng hoặc không tương thích."
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "Phiên bản modpack này không chứa tệp có thể tải xuống. Nó có thể không được làm đúng cách."
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "Có lỗi lạ xảy ra trong khi tải."
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Phiên bản minecraft hoặc loader hiện không được hỗ trợ bởi Modrinth Hosting."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "Đang cài addon..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "Đang cài modpack..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "Đang cài platform..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "Chúng tôi đang sửa máy chủ của bạn"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "Đang thêm Java..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "Đang chỉnh sửa máy chủ..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "Đang tải mod..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "Đang sắp sếp tệp..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "Đang thiết lập môi trường..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "Đã có máy chủ rồi?"
|
||||
},
|
||||
|
||||
@@ -4559,9 +4559,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "正在安装服务器"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "正在同步內容"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "同时发送好友邀请"
|
||||
},
|
||||
@@ -4616,51 +4613,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "安装失败"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "安装平台时发生内部错误,请重试。"
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "指定的加载器或 Minecraft 版本无法安装。它可能无效或不受支持。"
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "整合包无法安装。它可能已损坏或不兼容。"
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "这个整合包版本不包含可下载的文件,可能是打包方式有问题。"
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "安装过程中发生意外错误。"
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Modrinth Hosting 目前尚不支持此版本的 Minecraft 或加载器。"
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "正在安装组件……"
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "正在安装整合包……"
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "正在安装平台……"
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "我们正在准备你的服务器"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "正在添加 Java……"
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "正在配置服务器……"
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "正在下载模组……"
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "正在整理文件……"
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "正在设置运行环境……"
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "已经有服务器了吗?"
|
||||
},
|
||||
|
||||
@@ -4559,9 +4559,6 @@
|
||||
"servers.busy.installing": {
|
||||
"defaultMessage": "正在安裝伺服器"
|
||||
},
|
||||
"servers.busy.syncing-content": {
|
||||
"defaultMessage": "正在同步內容"
|
||||
},
|
||||
"servers.grant-access-modal.add-as-friend": {
|
||||
"defaultMessage": "同時傳送好友邀請"
|
||||
},
|
||||
@@ -4616,51 +4613,12 @@
|
||||
"servers.installing-banner.error.header": {
|
||||
"defaultMessage": "無法安裝"
|
||||
},
|
||||
"servers.installing-banner.error.internal-platform": {
|
||||
"defaultMessage": "安裝平臺時發生內部錯誤,請再試一次。"
|
||||
},
|
||||
"servers.installing-banner.error.invalid-loader-version": {
|
||||
"defaultMessage": "無法安裝指定的載入器或 Minecraft 版本。該載入器或版本可能無效或不受支援。"
|
||||
},
|
||||
"servers.installing-banner.error.modpack-install-failed": {
|
||||
"defaultMessage": "無法安裝模組包。該模組包可能已損毀或不相容。"
|
||||
},
|
||||
"servers.installing-banner.error.no-primary-file": {
|
||||
"defaultMessage": "這個模組包版本不包含可下載檔案。可能是打包時出了問題。"
|
||||
},
|
||||
"servers.installing-banner.error.unknown": {
|
||||
"defaultMessage": "安裝時發生非預期的錯誤。"
|
||||
},
|
||||
"servers.installing-banner.error.unsupported-loader-version": {
|
||||
"defaultMessage": "Modrinth Hosting 尚不支援這個 Minecraft 版本或載入器。"
|
||||
},
|
||||
"servers.installing-banner.phase.installing-addons": {
|
||||
"defaultMessage": "正在安裝額外內容..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-modpack": {
|
||||
"defaultMessage": "正在安裝模組包..."
|
||||
},
|
||||
"servers.installing-banner.phase.installing-platform": {
|
||||
"defaultMessage": "正在安裝平臺..."
|
||||
},
|
||||
"servers.installing-banner.preparing.header": {
|
||||
"defaultMessage": "我們正在為你準備伺服器"
|
||||
},
|
||||
"servers.installing-banner.ticker.adding-java": {
|
||||
"defaultMessage": "正在新增 Java..."
|
||||
},
|
||||
"servers.installing-banner.ticker.configuring-server": {
|
||||
"defaultMessage": "正在設定伺服器..."
|
||||
},
|
||||
"servers.installing-banner.ticker.downloading-mods": {
|
||||
"defaultMessage": "正在下載模組..."
|
||||
},
|
||||
"servers.installing-banner.ticker.organizing-files": {
|
||||
"defaultMessage": "正在整理檔案..."
|
||||
},
|
||||
"servers.installing-banner.ticker.setting-up-environment": {
|
||||
"defaultMessage": "正在設定環境..."
|
||||
},
|
||||
"servers.list-empty.already-have-server-label": {
|
||||
"defaultMessage": "已經有伺服器了嗎?"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,10 @@ import type { Archon, UploadState } from '@modrinth/api-client'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
|
||||
import type { MessageDescriptor } from '#ui/composables/i18n'
|
||||
import type {
|
||||
ServerInstallationKey,
|
||||
ServerInstallationState,
|
||||
} from '#ui/composables/server-installation-tracker'
|
||||
import type { FileOperation } from '#ui/layouts/shared/files-tab/types'
|
||||
|
||||
import { createContext } from '.'
|
||||
@@ -49,9 +53,11 @@ export interface ModrinthServerContext {
|
||||
readonly isServerRunning: ComputedRef<boolean>
|
||||
readonly stats: Ref<ServerStats>
|
||||
readonly uptimeSeconds: Ref<number>
|
||||
|
||||
// Content sync state
|
||||
readonly isSyncingContent: Ref<boolean>
|
||||
readonly installProgressItems: Ref<Archon.Websocket.v0.InstallProgressItem[]>
|
||||
readonly installation: ComputedRef<ServerInstallationState | null>
|
||||
beginInstallation: (key: ServerInstallationKey) => void
|
||||
cancelOptimisticInstallation: () => void
|
||||
dismissInstallation: (id: string) => void
|
||||
|
||||
// Busy state — when non-empty, all write operations should be disabled
|
||||
readonly busyReasons: ComputedRef<BusyReason[]>
|
||||
|
||||
@@ -71,7 +71,11 @@ const meta = {
|
||||
isServerRunning: computed(() => true),
|
||||
stats,
|
||||
uptimeSeconds: ref(0),
|
||||
isSyncingContent: ref(false),
|
||||
installProgressItems: ref<Archon.Websocket.v0.InstallProgressItem[]>([]),
|
||||
installation: computed(() => null),
|
||||
beginInstallation: () => {},
|
||||
cancelOptimisticInstallation: () => {},
|
||||
dismissInstallation: () => {},
|
||||
busyReasons: computed(() => []),
|
||||
fsAuth: ref(null),
|
||||
fsOps: ref<Archon.Websocket.v0.FilesystemOperation[]>([]),
|
||||
|
||||
@@ -1,6 +1,85 @@
|
||||
import type { Archon, UploadState } from '@modrinth/api-client'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import InstallingBanner from '../../components/servers/InstallingBanner.vue'
|
||||
import type { ServerInstallationState } from '../../composables/server-installation-tracker'
|
||||
import { provideModrinthServerContext } from '../../providers'
|
||||
import type {
|
||||
CancelUploadHandler,
|
||||
ModrinthServerContext,
|
||||
ServerStats,
|
||||
} from '../../providers/server-context'
|
||||
|
||||
function renderInstallation(state: ServerInstallationState) {
|
||||
return () => ({
|
||||
components: { InstallingBanner },
|
||||
setup() {
|
||||
const installation = ref<ServerInstallationState | null>(state)
|
||||
const serverContext: ModrinthServerContext = {
|
||||
get serverId() {
|
||||
return 'story-server'
|
||||
},
|
||||
worldId: ref<string | null>('story-world'),
|
||||
server: ref({
|
||||
server_id: 'story-server',
|
||||
status: 'installing',
|
||||
} as Archon.Servers.v0.Server),
|
||||
serverFull: computed(() => null),
|
||||
currentUserPermissions: computed(() => 0),
|
||||
isConnected: ref(true),
|
||||
isWsAuthIncorrect: ref(false),
|
||||
powerState: ref('stopped'),
|
||||
powerStateDetails: ref(undefined),
|
||||
isServerRunning: computed(() => false),
|
||||
stats: ref<ServerStats>({
|
||||
current: {
|
||||
cpu_percent: 0,
|
||||
ram_usage_bytes: 0,
|
||||
ram_total_bytes: 1,
|
||||
storage_usage_bytes: 0,
|
||||
storage_total_bytes: 0,
|
||||
},
|
||||
past: {
|
||||
cpu_percent: 0,
|
||||
ram_usage_bytes: 0,
|
||||
ram_total_bytes: 1,
|
||||
storage_usage_bytes: 0,
|
||||
storage_total_bytes: 0,
|
||||
},
|
||||
graph: { cpu: [], ram: [] },
|
||||
}),
|
||||
uptimeSeconds: ref(0),
|
||||
installProgressItems: ref<Archon.Websocket.v0.InstallProgressItem[]>([]),
|
||||
installation: computed(() => installation.value),
|
||||
beginInstallation: () => {},
|
||||
cancelOptimisticInstallation: () => {},
|
||||
dismissInstallation: () => {
|
||||
installation.value = null
|
||||
},
|
||||
busyReasons: computed(() => []),
|
||||
fsAuth: ref(null),
|
||||
fsOps: ref<Archon.Websocket.v0.FilesystemOperation[]>([]),
|
||||
fsQueuedOps: ref<Archon.Websocket.v0.QueuedFilesystemOp[]>([]),
|
||||
refreshFsAuth: async () => {},
|
||||
uploadState: ref<UploadState>({
|
||||
isUploading: false,
|
||||
currentFileName: null,
|
||||
currentFileProgress: 0,
|
||||
uploadedBytes: 0,
|
||||
totalBytes: 0,
|
||||
completedFiles: 0,
|
||||
totalFiles: 0,
|
||||
}),
|
||||
cancelUpload: ref<CancelUploadHandler | null>(null),
|
||||
activeOperations: computed(() => []),
|
||||
dismissOperation: async () => {},
|
||||
}
|
||||
provideModrinthServerContext(serverContext)
|
||||
},
|
||||
template: '<InstallingBanner />',
|
||||
})
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: 'Servers/InstallingBanner',
|
||||
@@ -10,109 +89,110 @@ const meta = {
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
name: 'Default (no progress)',
|
||||
}
|
||||
|
||||
export const WithProgress: Story = {
|
||||
args: {
|
||||
progress: {
|
||||
phase: 'InstallingLoader',
|
||||
percent: 45,
|
||||
export const PlatformPending: Story = {
|
||||
render: renderInstallation({
|
||||
id: 'platform:fabric:0.16.14:1.21.1',
|
||||
key: {
|
||||
type: 'platform',
|
||||
platform: 'fabric',
|
||||
platform_version: '0.16.14',
|
||||
game_version: '1.21.1',
|
||||
},
|
||||
},
|
||||
status: 'pending',
|
||||
progress: null,
|
||||
error: null,
|
||||
source: 'optimistic',
|
||||
}),
|
||||
}
|
||||
|
||||
export const IndeterminateLoaderInstall: Story = {
|
||||
args: {
|
||||
progress: {
|
||||
phase: 'InstallingLoader',
|
||||
percent: 0,
|
||||
export const PlatformProgress: Story = {
|
||||
render: renderInstallation({
|
||||
id: 'platform:fabric:0.16.14:1.21.1',
|
||||
key: {
|
||||
type: 'platform',
|
||||
platform: 'fabric',
|
||||
platform_version: '0.16.14',
|
||||
game_version: '1.21.1',
|
||||
},
|
||||
},
|
||||
status: 'installing',
|
||||
progress: 45,
|
||||
error: null,
|
||||
source: 'websocket',
|
||||
}),
|
||||
}
|
||||
|
||||
export const InstallingModpack: Story = {
|
||||
args: {
|
||||
progress: {
|
||||
phase: 'InstallingPack',
|
||||
percent: 72,
|
||||
export const Vanilla: Story = {
|
||||
render: renderInstallation({
|
||||
id: 'platform:vanilla::1.21.1',
|
||||
key: {
|
||||
type: 'platform',
|
||||
platform: 'vanilla',
|
||||
platform_version: '',
|
||||
game_version: '1.21.1',
|
||||
},
|
||||
},
|
||||
status: 'installing',
|
||||
progress: 20,
|
||||
error: null,
|
||||
source: 'websocket',
|
||||
}),
|
||||
}
|
||||
|
||||
export const InstallingAddons: Story = {
|
||||
args: {
|
||||
progress: {
|
||||
phase: 'Addons',
|
||||
percent: 90,
|
||||
export const Modpack: Story = {
|
||||
render: renderInstallation({
|
||||
id: 'modrinth-modpack:project:version',
|
||||
key: {
|
||||
type: 'modrinth_modpack',
|
||||
project_id: 'project',
|
||||
version_id: 'version',
|
||||
},
|
||||
},
|
||||
status: 'installing',
|
||||
progress: 72,
|
||||
error: null,
|
||||
source: 'websocket',
|
||||
}),
|
||||
}
|
||||
|
||||
export const ErrorInvalidVersion: Story = {
|
||||
name: 'Error: Invalid Version',
|
||||
args: {
|
||||
contentError: {
|
||||
step: 'modloader',
|
||||
description: 'the specified version may be incorrect',
|
||||
export const PlatformFailed: Story = {
|
||||
render: renderInstallation({
|
||||
id: 'platform:quilt:0.30.1-beta.2:1.14.4',
|
||||
key: {
|
||||
type: 'platform',
|
||||
platform: 'quilt',
|
||||
platform_version: '0.30.1-beta.2',
|
||||
game_version: '1.14.4',
|
||||
},
|
||||
},
|
||||
status: 'failed',
|
||||
progress: null,
|
||||
error: 'Platform installer failed',
|
||||
source: 'websocket',
|
||||
}),
|
||||
}
|
||||
|
||||
export const ErrorUnsupportedVersion: Story = {
|
||||
name: 'Error: Unsupported Version',
|
||||
args: {
|
||||
contentError: {
|
||||
step: 'modloader',
|
||||
description: 'this version is not yet supported',
|
||||
export const ModpackFailed: Story = {
|
||||
render: renderInstallation({
|
||||
id: 'modrinth-modpack:project:version',
|
||||
key: {
|
||||
type: 'modrinth_modpack',
|
||||
project_id: 'project',
|
||||
version_id: 'version',
|
||||
},
|
||||
},
|
||||
status: 'failed',
|
||||
progress: null,
|
||||
error: 'The modpack does not include a downloadable file',
|
||||
source: 'websocket',
|
||||
}),
|
||||
}
|
||||
|
||||
export const ErrorInternal: Story = {
|
||||
name: 'Error: Internal',
|
||||
args: {
|
||||
contentError: {
|
||||
step: 'modloader',
|
||||
description: 'internal error',
|
||||
export const LocalModpackFailed: Story = {
|
||||
render: renderInstallation({
|
||||
id: 'local-modpack:example.mrpack',
|
||||
key: {
|
||||
type: 'local_modpack',
|
||||
filename: 'example.mrpack',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const ErrorModpackInstall: Story = {
|
||||
name: 'Error: Modpack Install Failed',
|
||||
args: {
|
||||
contentError: {
|
||||
step: 'modpack',
|
||||
description: 'Failed to install modpack',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const ErrorModpackNoFile: Story = {
|
||||
name: 'Error: Modpack No Primary File',
|
||||
args: {
|
||||
contentError: {
|
||||
step: 'modpack',
|
||||
description: 'Modpack version has no primary file',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const AllStates: Story = {
|
||||
render: () => ({
|
||||
components: { InstallingBanner },
|
||||
template: /*html*/ `
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<InstallingBanner />
|
||||
<InstallingBanner :progress="{ phase: 'InstallingLoader', percent: 0 }" />
|
||||
<InstallingBanner :progress="{ phase: 'InstallingLoader', percent: 45 }" />
|
||||
<InstallingBanner :content-error="{ step: 'modloader', description: 'the specified version may be incorrect' }" />
|
||||
<InstallingBanner :content-error="{ step: 'modloader', description: 'this version is not yet supported' }" />
|
||||
<InstallingBanner :content-error="{ step: 'modloader', description: 'internal error' }" />
|
||||
<InstallingBanner :content-error="{ step: 'modpack', description: 'Failed to install modpack' }" />
|
||||
</div>
|
||||
`,
|
||||
status: 'failed',
|
||||
progress: null,
|
||||
error: 'The modpack could not be read',
|
||||
source: 'websocket',
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -88,7 +88,11 @@ const meta = {
|
||||
isServerRunning: computed(() => true),
|
||||
stats,
|
||||
uptimeSeconds: ref(0),
|
||||
isSyncingContent: ref(false),
|
||||
installProgressItems: ref<Archon.Websocket.v0.InstallProgressItem[]>([]),
|
||||
installation: computed(() => null),
|
||||
beginInstallation: () => {},
|
||||
cancelOptimisticInstallation: () => {},
|
||||
dismissInstallation: () => {},
|
||||
busyReasons: computed(() => [
|
||||
{ reason: defineMessage({ id: 's.bg', defaultMessage: 'Background task running' }) },
|
||||
]),
|
||||
|
||||
@@ -11,7 +11,6 @@ export * from './notices'
|
||||
export * from './project-types'
|
||||
export * from './savable'
|
||||
export * from './search'
|
||||
export * from './server-content-installing'
|
||||
export * from './server-search'
|
||||
export * from './tag-messages'
|
||||
export * from './truncate'
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
import type {
|
||||
ContentCardProject,
|
||||
ContentCardVersion,
|
||||
ContentOwner,
|
||||
} from '../layouts/shared/content-tab/types'
|
||||
|
||||
export type PendingServerContentInstallType = 'mod' | 'plugin' | 'datapack'
|
||||
type PendingServerContentOwner = Omit<ContentOwner, 'link'> & { link?: string }
|
||||
|
||||
export interface PendingServerContentInstall {
|
||||
projectId: string
|
||||
versionId: string
|
||||
contentType: PendingServerContentInstallType
|
||||
title: ContentCardProject['title']
|
||||
versionName?: ContentCardVersion['version_number'] | null
|
||||
versionNumber?: ContentCardVersion['version_number'] | null
|
||||
fileName?: ContentCardVersion['file_name'] | null
|
||||
owner?: PendingServerContentOwner | null
|
||||
slug?: ContentCardProject['slug'] | null
|
||||
iconUrl?: ContentCardProject['icon_url'] | null
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
interface PendingServerContentInstallBaseline {
|
||||
contentKeys: string[]
|
||||
projectIds?: string[]
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
export const pendingServerContentInstallsEvent = 'modrinth:pending-server-content-installs'
|
||||
|
||||
const stalePendingInstallAge = 30 * 60 * 1000
|
||||
|
||||
function getPendingServerContentInstallsKey(serverId: string | null, worldId: string | null) {
|
||||
if (!serverId || !worldId) return null
|
||||
return `server-content-installing:${serverId}:${worldId}`
|
||||
}
|
||||
|
||||
function getPendingServerContentInstallBaselineKey(
|
||||
serverId: string | null,
|
||||
worldId: string | null,
|
||||
) {
|
||||
if (!serverId || !worldId) return null
|
||||
return `server-content-installing-baseline:${serverId}:${worldId}`
|
||||
}
|
||||
|
||||
function isPendingServerContentInstall(value: unknown): value is PendingServerContentInstall {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
const record = value as Record<string, unknown>
|
||||
return (
|
||||
typeof record.projectId === 'string' &&
|
||||
typeof record.versionId === 'string' &&
|
||||
(record.contentType === 'mod' ||
|
||||
record.contentType === 'plugin' ||
|
||||
record.contentType === 'datapack') &&
|
||||
typeof record.title === 'string' &&
|
||||
typeof record.createdAt === 'number'
|
||||
)
|
||||
}
|
||||
|
||||
function isPendingServerContentInstallBaseline(
|
||||
value: unknown,
|
||||
): value is PendingServerContentInstallBaseline {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
const record = value as Record<string, unknown>
|
||||
const contentKeys = record.contentKeys ?? record.projectIds
|
||||
return (
|
||||
Array.isArray(contentKeys) &&
|
||||
contentKeys.every((contentKey) => typeof contentKey === 'string') &&
|
||||
typeof record.createdAt === 'number'
|
||||
)
|
||||
}
|
||||
|
||||
function filterFreshPendingServerContentInstalls(items: PendingServerContentInstall[]) {
|
||||
const cutoff = Date.now() - stalePendingInstallAge
|
||||
return items.filter((item) => item.createdAt >= cutoff)
|
||||
}
|
||||
|
||||
function isFreshPendingServerContentInstallBaseline(item: PendingServerContentInstallBaseline) {
|
||||
return item.createdAt >= Date.now() - stalePendingInstallAge
|
||||
}
|
||||
|
||||
function emitPendingServerContentInstallsChanged(serverId: string | null, worldId: string | null) {
|
||||
if (typeof window === 'undefined') return
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(pendingServerContentInstallsEvent, {
|
||||
detail: { serverId, worldId },
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function readPendingServerContentInstalls(serverId: string | null, worldId: string | null) {
|
||||
const key = getPendingServerContentInstallsKey(serverId, worldId)
|
||||
if (!key || typeof localStorage === 'undefined') return []
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(key)
|
||||
if (!raw) return []
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!Array.isArray(parsed)) return []
|
||||
const freshItems = filterFreshPendingServerContentInstalls(
|
||||
parsed.filter(isPendingServerContentInstall),
|
||||
)
|
||||
if (freshItems.length !== parsed.length) {
|
||||
writePendingServerContentInstalls(serverId, worldId, freshItems)
|
||||
}
|
||||
return freshItems
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function writePendingServerContentInstalls(
|
||||
serverId: string | null,
|
||||
worldId: string | null,
|
||||
items: PendingServerContentInstall[],
|
||||
) {
|
||||
const key = getPendingServerContentInstallsKey(serverId, worldId)
|
||||
if (!key || typeof localStorage === 'undefined') return
|
||||
|
||||
const freshItems = filterFreshPendingServerContentInstalls(items)
|
||||
if (freshItems.length === 0) {
|
||||
localStorage.removeItem(key)
|
||||
const baselineKey = getPendingServerContentInstallBaselineKey(serverId, worldId)
|
||||
if (baselineKey) {
|
||||
localStorage.removeItem(baselineKey)
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem(key, JSON.stringify(freshItems))
|
||||
}
|
||||
emitPendingServerContentInstallsChanged(serverId, worldId)
|
||||
}
|
||||
|
||||
export function readPendingServerContentInstallBaseline(
|
||||
serverId: string | null,
|
||||
worldId: string | null,
|
||||
) {
|
||||
const key = getPendingServerContentInstallBaselineKey(serverId, worldId)
|
||||
if (!key || typeof localStorage === 'undefined') return null
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(key)
|
||||
if (!raw) return null
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!isPendingServerContentInstallBaseline(parsed)) return null
|
||||
if (!isFreshPendingServerContentInstallBaseline(parsed)) {
|
||||
localStorage.removeItem(key)
|
||||
return null
|
||||
}
|
||||
return new Set(parsed.contentKeys ?? parsed.projectIds)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function writePendingServerContentInstallBaseline(
|
||||
serverId: string | null,
|
||||
worldId: string | null,
|
||||
contentKeys: Iterable<string>,
|
||||
) {
|
||||
const key = getPendingServerContentInstallBaselineKey(serverId, worldId)
|
||||
if (!key || typeof localStorage === 'undefined') return
|
||||
|
||||
localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
contentKeys: Array.from(new Set(contentKeys)),
|
||||
createdAt: Date.now(),
|
||||
} satisfies PendingServerContentInstallBaseline),
|
||||
)
|
||||
emitPendingServerContentInstallsChanged(serverId, worldId)
|
||||
}
|
||||
|
||||
export function addPendingServerContentInstalls(
|
||||
serverId: string | null,
|
||||
worldId: string | null,
|
||||
items: Omit<PendingServerContentInstall, 'createdAt'>[],
|
||||
) {
|
||||
if (items.length === 0) return
|
||||
|
||||
const now = Date.now()
|
||||
const next = new Map(
|
||||
readPendingServerContentInstalls(serverId, worldId).map((item) => [item.projectId, item]),
|
||||
)
|
||||
for (const item of items) {
|
||||
next.set(item.projectId, { ...item, createdAt: now })
|
||||
}
|
||||
writePendingServerContentInstalls(serverId, worldId, Array.from(next.values()))
|
||||
}
|
||||
|
||||
export function removePendingServerContentInstall(
|
||||
serverId: string | null,
|
||||
worldId: string | null,
|
||||
projectId: string,
|
||||
) {
|
||||
writePendingServerContentInstalls(
|
||||
serverId,
|
||||
worldId,
|
||||
readPendingServerContentInstalls(serverId, worldId).filter(
|
||||
(item) => item.projectId !== projectId,
|
||||
),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user