feat: complete hosted mod sync and launcher interface updates
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled

Add pack sync markers, tagged mod updates, parallel progress, JWT downloads and retry recovery. Include pending onboarding, about scene, compatibility data pack and download fixes.
This commit is contained in:
Disy committed 2026-09-15 19:06:56 +08:00
1 parent 5d473ebfbc
commit bc904065c3
63 files changed
+3516 -1059

No files matched your search

+79 -11
View File
@@ -1,25 +1,61 @@
<script setup lang="ts">
import { FolderOpenIcon, LeftArrowIcon, SparklesIcon } from '@modrinth/assets'
import { BigOptionButton, Button, defineMessages, useVIntl } from '@modrinth/ui'
import { inject, ref } from 'vue'
import { inject, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import InstanceModeOptions from '@/components/instance/InstanceModeOptions.vue'
import HostedPackProgress from '@/components/instance/HostedPackProgress.vue'
import type { InstanceMode } from '@/helpers/hosted-packs'
import { useHostedCreation } from '@/composables/useHostedCreation'
const { formatMessage } = useVIntl()
const router = useRouter()
const instanceMode = ref<InstanceMode>('local')
const { installing, installError, createdInstance, completed, acknowledge, install } =
useHostedCreation()
const instanceMode = ref<InstanceMode>(
installing.value || createdInstance.value ? 'starlight' : 'local',
)
async function openCompleted(instanceId: string) {
try {
const failure = await router.push(`/instance/${encodeURIComponent(instanceId)}/`)
if (!failure) acknowledge(instanceId)
else installError.value = failure.message
} catch (cause) {
installError.value = String(cause)
}
}
watch(
() => (completed.value ? createdInstance.value : undefined),
(instanceId) => {
if (instanceId) void openCompleted(instanceId)
},
{ immediate: true },
)
const showModal = inject<
(options?: {
skipSetupType?: boolean
initialMode?: 'custom' | 'import'
instanceMode?: InstanceMode
onBack?: () => void
}) => void
>('showCreationModalWithOptions')
const messages = defineMessages({
openInstance: { id: 'app.hosted-packs.open-instance', defaultMessage: 'Open installed instance' },
autoInstall: {
id: 'app.hosted-packs.auto-install',
defaultMessage: 'Install the StarLight modpack',
},
autoDescription: {
id: 'app.hosted-packs.auto-description',
defaultMessage:
'Download and automatically install the modpack from StarLight to play on the StarLight server with one click.',
},
autoInstalling: {
id: 'app.hosted-packs.auto-installing',
defaultMessage: 'Downloading and installing the server modpack…',
},
retryInstall: { id: 'app.hosted-packs.retry-install', defaultMessage: 'Retry installation' },
title: {
id: 'create.title',
defaultMessage: 'Create Instance',
@@ -60,11 +96,19 @@ const messages = defineMessages({
const navigateBack = () => router.push('/library')
function handleStartFresh() {
async function handleStartFresh() {
if (installing.value) return
if (instanceMode.value === 'starlight') {
if (completed.value && createdInstance.value) {
await openCompleted(createdInstance.value)
return
}
await install()
return
}
showModal?.({
skipSetupType: true,
initialMode: 'custom',
instanceMode: instanceMode.value,
onBack: () => router.push('/create'),
})
}
@@ -73,7 +117,6 @@ function handleImportExisting() {
showModal?.({
skipSetupType: true,
initialMode: 'import',
instanceMode: 'local',
onBack: () => router.push('/create'),
})
}
@@ -91,13 +134,36 @@ function handleImportExisting() {
</p>
</div>
<InstanceModeOptions v-model="instanceMode" data-onboarding-id="creation-instance-mode" />
<InstanceModeOptions
v-model="instanceMode"
:disabled="installing"
data-onboarding-id="creation-instance-mode"
/>
<div data-onboarding-id="creation-methods" class="flex flex-col gap-4 sm:flex-row">
<BigOptionButton
data-onboarding-id="creation-method-custom"
:data-onboarding-id="
instanceMode === 'starlight' ? 'creation-method-starlight' : 'creation-method-custom'
"
:disabled="installing"
:icon="SparklesIcon"
:title="formatMessage(messages.newTitle)"
:description="formatMessage(messages.newDescription)"
:title="
formatMessage(
instanceMode === 'starlight'
? installing
? messages.autoInstalling
: completed
? messages.openInstance
: createdInstance
? messages.retryInstall
: messages.autoInstall
: messages.newTitle,
)
"
:description="
formatMessage(
instanceMode === 'starlight' ? messages.autoDescription : messages.newDescription,
)
"
no-icon-box
@click="handleStartFresh"
/>
@@ -113,7 +179,9 @@ function handleImportExisting() {
/>
</div>
<p class="m-0 text-sm text-secondary">
<HostedPackProgress :instance-id="createdInstance" :active="installing" />
<p v-if="installError" class="m-0 text-red" role="alert">{{ installError }}</p>
<p v-if="instanceMode === 'local'" class="m-0 text-sm text-secondary">
{{ formatMessage(messages.pclHmclHint) }}
{{ ' ' }}
<RouterLink
+60 -24
View File
@@ -72,22 +72,38 @@
</div>
<div class="min-w-0 flex-grow">
<div class="truncate font-semibold text-contrast">{{ bar.title || bar.message }}</div>
<div class="truncate text-sm text-secondary">{{ bar.message }}</div>
<div class="break-words text-sm text-secondary">{{ bar.message }}</div>
</div>
<TagItem>
<TagItem
v-if="['hosted_pack_sync', 'hosted_mod_download'].includes(bar.bar_type?.type ?? '')"
>StarLight</TagItem
>
<TagItem v-else>
<component :is="providerIcon(legacyProvider(bar))" />
{{ providerLabel(legacyProvider(bar)) }}
</TagItem>
<Badge color="orange" :type="statusLabel('running')" />
<Badge
:color="bar.bar_type?.error ? 'red' : 'orange'"
:type="statusLabel(bar.bar_type?.error ? 'failed' : 'running')"
/>
</div>
<ProgressBar
v-if="!bar.bar_type?.error"
class="mt-4"
full-width
:progress="legacyPercent(bar)"
:max="100"
:label="formatMessage(messages.progress)"
show-progress
:waiting="!bar.total"
:show-progress="Boolean(bar.total)"
/>
<RouterLink
v-else
:to="`/instance/${encodeURIComponent(bar.bar_type?.instance_id ?? '')}/mods`"
class="mt-3 inline-block text-brand hover:underline"
>
{{ formatMessage(messages.hostedRetry) }}
</RouterLink>
</Card>
<Card
@@ -140,10 +156,7 @@
v-if="downloadDetails(job).length"
class="mt-1 flex flex-wrap items-center gap-2 text-sm text-secondary"
>
<template
v-for="(metric, index) in downloadDetails(job)"
:key="`${index}-${metric}`"
>
<template v-for="(metric, index) in downloadDetails(job)" :key="`${index}-${metric}`">
<BulletDivider v-if="index > 0" />
<span>{{ metric }}</span>
</template>
@@ -389,10 +402,10 @@
<Card v-else class="flex flex-1">
<EmptyState
class="my-auto"
:type="query ? 'no-search-result' : 'no-tasks'"
:heading="formatMessage(query ? messages.noResultsTitle : messages.emptyTitle)"
:type="hasFilters ? 'no-search-result' : 'no-tasks'"
:heading="formatMessage(hasFilters ? messages.noResultsTitle : messages.emptyTitle)"
:description="
formatMessage(query ? messages.noResultsDescription : messages.emptyDescription)
formatMessage(hasFilters ? messages.noResultsDescription : messages.emptyDescription)
"
/>
</Card>
@@ -503,6 +516,10 @@ const focusedJobId = computed(() => focusedDownloadJobId(route.query.job))
const focusState = ref(createDownloadFocusState(focusedJobId.value))
const messages = defineMessages({
hostedRetry: {
id: 'app.hosted-packs.progress.retry',
defaultMessage: 'Return to the instance to retry',
},
newDownload: { id: 'app.downloads.new-download', defaultMessage: 'New download' },
inProgress: { id: 'app.downloads.in-progress', defaultMessage: 'In progress' },
history: { id: 'app.downloads.history', defaultMessage: 'History' },
@@ -727,10 +744,22 @@ const phaseMessages = defineMessages({
rolling_back: { id: 'app.downloads.phase.rolling-back', defaultMessage: 'Rolling back changes' },
} satisfies Record<InstallPhaseId, MessageDescriptor>)
const legacyDownloads = manager.legacyDownloads
const legacyDownloads = computed(() => {
const normalized = query.value.trim().toLowerCase()
return manager.legacyDownloads.value.filter((bar) => {
if (provider.value !== 'all' && legacyProvider(bar) !== provider.value) return false
return (
!normalized ||
[bar.title, bar.message, bar.bar_type?.instance_name, bar.bar_type?.file_name].some((value) =>
value?.toLowerCase().includes(normalized),
)
)
})
})
const historyJobs = manager.historyJobs
const providerOptions = [
'all',
'starlight',
'modrinth',
'curse_forge',
'minecraft',
@@ -739,6 +768,12 @@ const providerOptions = [
'local',
]
const historyStatusOptions = ['all', 'succeeded', 'failed', 'interrupted', 'canceled']
const hasFilters = computed(
() =>
Boolean(query.value.trim()) ||
provider.value !== 'all' ||
(tab.value === 'history' && historyStatus.value !== 'all'),
)
const downloadTabs = computed(() => [
{
href: 'active',
@@ -786,8 +821,9 @@ function displayIcon(icon: string) {
return /^(https?:|data:|blob:|asset:|tauri:)/.test(icon) ? icon : convertFileSrc(icon)
}
function providerLabel(value: InstallJobSnapshot['provider']) {
function providerLabel(value: InstallJobSnapshot['provider'] | 'starlight') {
return {
starlight: 'StarLight',
modrinth: 'Modrinth',
curse_forge: 'CurseForge',
minecraft: 'Minecraft',
@@ -800,10 +836,10 @@ function providerLabel(value: InstallJobSnapshot['provider']) {
function providerFilterLabel(value: string) {
return value === 'all'
? formatMessage(messages.allSources)
: providerLabel(value as InstallJobSnapshot['provider'])
: providerLabel(value as InstallJobSnapshot['provider'] | 'starlight')
}
function providerIcon(value: InstallJobSnapshot['provider']) {
function providerIcon(value: InstallJobSnapshot['provider'] | 'starlight') {
return value === 'curse_forge'
? CurseForgeIcon
: value === 'modrinth'
@@ -821,7 +857,9 @@ function jobTypeIcon(job: InstallJobSnapshot) {
return job.kind === 'upgrade_unmanaged_instance' ? RefreshCwIcon : providerIcon(job.provider)
}
function legacyProvider(bar: LoadingBar): InstallJobSnapshot['provider'] {
function legacyProvider(bar: LoadingBar): InstallJobSnapshot['provider'] | 'starlight' {
if (['hosted_pack_sync', 'hosted_mod_download'].includes(bar.bar_type?.type ?? ''))
return 'starlight'
if (bar.bar_type?.type === 'pack_download' || bar.bar_type?.type === 'pack_file_download')
return 'curse_forge'
if (bar.bar_type?.type === 'minecraft_download') return 'minecraft'
@@ -1254,14 +1292,12 @@ async function resolveMissing(job: InstallJobSnapshot) {
(item) =>
item.status === 'skipped' && item.manual_url && item.project_id && item.version_id,
)
.map(
(item): CurseForgeManualDownloadItem => ({
projectId: Number(item.project_id),
fileId: Number(item.version_id),
fileName: item.name,
websiteUrl: item.manual_url ?? undefined,
}),
)
.map((item): CurseForgeManualDownloadItem => ({
projectId: Number(item.project_id),
fileId: Number(item.version_id),
fileName: item.name,
websiteUrl: item.manual_url ?? undefined,
}))
const instanceId = job.instance_id
const hasGeneralMissingItems = job.items.some((item) => item.status === 'failed')
if (instanceId && (job.provider === 'curse_forge' || fallbackCurseForgeItems.length > 0)) {
@@ -417,6 +417,7 @@ import {
import { useInstanceConsole } from '@/composables/useInstanceConsole'
import { useMinecraftLaunchError } from '@/composables/useMinecraftLaunchError'
import { useNetworkStatus } from '@/composables/useNetworkStatus'
import { useInstanceMode } from '@/composables/useInstanceMode'
import { postUpgradeNoticeQueryKey, usePostUpgradeNotice } from '@/composables/usePostUpgradeNotice'
import { useSymlinkWarningDismiss } from '@/composables/useSymlinkWarningDismiss'
import { get_project_v3 } from '@/helpers/cache.js'
@@ -514,6 +515,7 @@ const { offline } = useNetworkStatus()
const instance = ref<GameInstance>()
const instanceId = computed(() => instance.value?.id)
const instanceModeQuery = useInstanceMode(() => instance.value?.id ?? props.id)
const postUpgradeNoticeQuery = usePostUpgradeNotice(() => instance.value?.id ?? props.id)
const postUpgradeNotice = computed(() => postUpgradeNoticeQuery.data.value ?? null)
const symlinkWarning = useSymlinkWarningDismiss(instanceId)
@@ -532,7 +534,9 @@ const isServerInstance = ref(false)
const linkedProjectV3 = ref<Labrinth.Projects.v3.Project>()
const selected = ref<unknown[]>([])
const canUpgradeInstance = computed(() =>
instance.value ? isUnmanagedUpgradeEligible(instance.value) : false,
instance.value && instanceModeQuery.data.value === 'local'
? isUnmanagedUpgradeEligible(instance.value)
: false,
)
const minecraftServer = computed(() => linkedProjectV3.value?.minecraft_server)
@@ -774,7 +778,6 @@ const startInstance = async (context: string) => {
launchElapsedTimer = undefined
loading.value = false
}
}
const stopInstance = async (context: string) => {