From 32ca8d549b63da8b5fab12ea0e14f5b9c147c013 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Sat, 13 Jun 2026 08:29:59 +0100 Subject: [PATCH] fix: better handling of datapacks in frontend & desync issues between browse page impl in app and website --- apps/app-frontend/src/pages/Browse.vue | 19 ++++- .../providers/setup/server-install-content.ts | 1 + .../composables/use-server-install-content.ts | 82 +++++++++++++------ .../src/pages/discover/[type]/index.vue | 26 +++++- .../browse-tab/composables/install-logic.ts | 35 +++++--- .../[id]/instances/[instance-id]/content.vue | 9 +- .../ui/src/utils/version-compatibility.ts | 8 +- 7 files changed, 136 insertions(+), 44 deletions(-) diff --git a/apps/app-frontend/src/pages/Browse.vue b/apps/app-frontend/src/pages/Browse.vue index 2ef0d66878..d0bf255914 100644 --- a/apps/app-frontend/src/pages/Browse.vue +++ b/apps/app-frontend/src/pages/Browse.vue @@ -24,6 +24,8 @@ import { provideBrowseManager, requestInstall, SelectedProjectsFloatingBar, + stripServerRuntimeInstallFilters, + stripServerRuntimeInstallOverrides, useBrowseSearch, useStickyObserver, useVIntl, @@ -497,7 +499,13 @@ watch( router.replace({ path: `/browse/${targetProjectType}`, - query: route.query, + query: { + sid: route.query.sid, + wid: route.query.wid, + shi: route.query.shi, + from: route.query.from, + q: route.query.q, + }, }) }, { immediate: true }, @@ -708,7 +716,6 @@ async function chooseInstanceInstallVersion( const selectedVersion = getLatestMatchingInstallVersion( await getInstallProjectVersions(project.project_id), selectedPreferences, - projectTypeValue, ) if (!selectedVersion) { @@ -790,11 +797,15 @@ function getCardActions( project: projectResult, contentType, mode: isModpack ? 'immediate' : 'queue', - selectedFilters: isModpack ? [] : searchState.currentFilters.value, + selectedFilters: isModpack + ? [] + : stripServerRuntimeInstallFilters(searchState.currentFilters.value), providedFilters: isModpack ? [] : combinedProvidedFilters.value, overriddenProvidedFilterTypes: isModpack ? [] - : searchState.overriddenProvidedFilterTypes.value, + : stripServerRuntimeInstallOverrides( + searchState.overriddenProvidedFilterTypes.value, + ), targetPreferences: getServerInstallTargetPreferences(contentType), getProjectVersions: getInstallProjectVersions, queue: serverInstallQueue, diff --git a/apps/app-frontend/src/providers/setup/server-install-content.ts b/apps/app-frontend/src/providers/setup/server-install-content.ts index 983fec9855..5961a18844 100644 --- a/apps/app-frontend/src/providers/setup/server-install-content.ts +++ b/apps/app-frontend/src/providers/setup/server-install-content.ts @@ -567,6 +567,7 @@ export function createServerInstallContent(opts: { plans.map((plan) => ({ project_id: plan.projectId, version_id: plan.versionId, + kind: plan.contentType as Archon.Content.v1.AddonKind, })), ), onQueueChange: (plans) => setStoredServerInstallPlans(serverId, worldId, plans), diff --git a/apps/frontend/src/composables/use-server-install-content.ts b/apps/frontend/src/composables/use-server-install-content.ts index 0dff3a74b1..c889551a89 100644 --- a/apps/frontend/src/composables/use-server-install-content.ts +++ b/apps/frontend/src/composables/use-server-install-content.ts @@ -21,6 +21,8 @@ import { readStoredServerInstallQueue, removePendingServerContentInstall, requestInstall, + stripServerRuntimeInstallFilters, + stripServerRuntimeInstallOverrides, useVIntl, writePendingServerContentInstallBaseline, writeStoredServerInstallQueue, @@ -217,6 +219,42 @@ export function useServerInstallContent({ enabled: computed(() => !!currentServerId.value && !!currentWorldId.value), }) + const currentWorld = computed(() => { + if (fromContext.value === 'create-instance') return null + + const full = serverFullData.value + if (!full) return null + + const worldId = currentWorldId.value + if (worldId) { + return full.worlds.find((world) => world.id === worldId) ?? null + } + + return full.worlds.find((world) => world.is_active) ?? full.worlds[0] ?? null + }) + const serverContextWorldGameVersion = computed(() => { + const worldGameVersion = currentWorld.value?.content?.game_version + if (currentWorldId.value) return worldGameVersion ?? null + return worldGameVersion ?? serverData.value?.mc_version ?? null + }) + const serverContextWorldLoader = computed(() => { + const worldLoader = currentWorld.value?.content?.modloader + if (currentWorldId.value) return worldLoader ?? null + return worldLoader ?? serverData.value?.loader ?? null + }) + const serverContextWorldLoaderVersion = computed(() => { + const worldLoaderVersion = currentWorld.value?.content?.modloader_version + if (currentWorldId.value) return worldLoaderVersion ?? null + return worldLoaderVersion ?? serverData.value?.loader_version ?? null + }) + const serverContentProjectType = computed(() => { + const loader = serverContextWorldLoader.value?.toLowerCase() + if (!loader) return null + if (loader === 'paper' || loader === 'purpur') return 'plugin' + if (loader === 'vanilla') return 'datapack' + return 'mod' + }) + function setBrowseSearchState(state: ServerInstallBrowseSearchState) { browseSearchState = state } @@ -353,12 +391,12 @@ export function useServerInstallContent({ ) const filters: FilterValue[] = [] if (serverData.value && projectType.value?.id !== 'modpack') { - const gameVersion = serverData.value.mc_version + const gameVersion = serverContextWorldGameVersion.value if (gameVersion) { filters.push({ type: 'game_version', option: gameVersion }) } - const platform = serverData.value.loader?.toLowerCase() + const platform = serverContextWorldLoader.value?.toLowerCase().replaceAll('_', '') const modLoaders = ['fabric', 'forge', 'quilt', 'neoforge'] if (platform && modLoaders.includes(platform)) { @@ -416,8 +454,8 @@ export function useServerInstallContent({ function getServerInstallTargetPreferences(contentType: BrowseInstallContentType) { return getTargetInstallPreferences( { - gameVersion: serverData.value?.mc_version, - loader: serverData.value?.loader, + gameVersion: serverContextWorldGameVersion.value, + loader: serverContextWorldLoader.value, }, contentType, ) @@ -434,7 +472,11 @@ export function useServerInstallContent({ async function resolveQueuedAddonPlans(plans: BrowseInstallPlan[]) { const existingProjectIds = getServerInstalledProjectIds() - const resolvedAddons: Array<{ project_id: string; version_id: string }> = [] + const resolvedAddons: Array<{ + project_id: string + version_id: string + kind: Archon.Content.v1.AddonKind + }> = [] for (const plan of plans) { const resolved = await client.labrinth.content_v3.resolve({ @@ -453,6 +495,7 @@ export function useServerInstallContent({ resolvedAddons.push({ project_id: item.project_id, version_id: item.version_id, + kind: plan.contentType as Archon.Content.v1.AddonKind, }) } } @@ -612,11 +655,15 @@ export function useServerInstallContent({ project, contentType, mode: isModpack ? 'immediate' : 'queue', - selectedFilters: isModpack ? [] : browseSearchState.currentFilters.value, + selectedFilters: isModpack + ? [] + : stripServerRuntimeInstallFilters(browseSearchState.currentFilters.value), providedFilters: isModpack ? [] : serverFilters.value, overriddenProvidedFilterTypes: isModpack ? [] - : browseSearchState.overriddenProvidedFilterTypes.value, + : stripServerRuntimeInstallOverrides( + browseSearchState.overriddenProvidedFilterTypes.value, + ), targetPreferences: getServerInstallTargetPreferences(contentType), getProjectVersions: getInstallProjectVersions, queue: serverInstallQueue, @@ -753,20 +800,6 @@ export function useServerInstallContent({ ? formatMessage(messages.createInstanceModpackHeading) : formatMessage(commonMessages.installingContentLabel), ) - const currentWorld = computed(() => { - if (fromContext.value === 'create-instance') return null - - const full = serverFullData.value - if (!full) return null - - const worldId = currentWorldId.value - if (worldId) { - return full.worlds.find((world) => world.id === worldId) ?? null - } - - return full.worlds.find((world) => world.is_active) ?? full.worlds[0] ?? null - }) - const installContext = computed(() => { if (!serverData.value) return null return { @@ -774,10 +807,10 @@ export function useServerInstallContent({ fromContext.value === 'create-instance' ? formatMessage(messages.createInstanceName) : (currentWorld.value?.name ?? formatMessage(messages.worldFallbackName)), - loader: currentWorld.value?.content?.modloader ?? serverData.value.loader ?? '', + loader: serverContextWorldLoader.value ?? '', loaderVersion: - currentWorld.value?.content?.modloader_version ?? serverData.value.loader_version ?? '', - gameVersion: currentWorld.value?.content?.game_version ?? serverData.value.mc_version ?? '', + serverContextWorldLoaderVersion.value ?? '', + gameVersion: serverContextWorldGameVersion.value ?? '', serverId: currentServerId.value, upstream: serverData.value.upstream, iconSrc: serverIcon.value, @@ -843,6 +876,7 @@ export function useServerInstallContent({ currentWorldId, serverData, serverContentData, + serverContentProjectType, serverFilters, serverHideInstalled, hideSelectedServerInstalls, diff --git a/apps/frontend/src/pages/discover/[type]/index.vue b/apps/frontend/src/pages/discover/[type]/index.vue index f5d25e0893..a487971eef 100644 --- a/apps/frontend/src/pages/discover/[type]/index.vue +++ b/apps/frontend/src/pages/discover/[type]/index.vue @@ -115,6 +115,7 @@ debug('initial route.params.type:', route.params.type, '→ currentType:', curre const isServerType = computed(() => currentType.value === 'server') const projectType = computed(() => tags.value.projectTypes.find((x) => x.id === currentType.value)) +const projectTypeId = computed(() => projectType.value?.id ?? 'mod') watch( () => projectType.value?.id, @@ -162,6 +163,7 @@ const { queuedServerInstallProjectIds, queuedServerInstallCount, isInstallingQueuedServerInstalls, + serverContentProjectType, installContext, setBrowseSearchState, syncHiddenInstalledProjectIds, @@ -174,6 +176,28 @@ const { onboardingModalRef, debug, }) + +watch( + [currentServerId, fromContext, projectTypeId, serverContentProjectType], + ([serverId, from, currentProjectType, targetProjectType]) => { + if (!serverId || from || !targetProjectType) return + if (!['mod', 'plugin', 'datapack'].includes(currentProjectType)) return + if (currentProjectType === targetProjectType) return + + navigateTo({ + path: `/discover/${targetProjectType}s`, + query: { + sid: route.query.sid, + wid: route.query.wid, + shi: route.query.shi, + from: route.query.from, + q: route.query.q, + }, + }) + }, + { immediate: true }, +) + const stickyInstallHeaderRef = ref(null) const { isStuck: isInstallHeaderStuck } = useStickyObserver( stickyInstallHeaderRef, @@ -411,8 +435,6 @@ const messages = defineMessages({ }, }) -const projectTypeId = computed(() => projectType.value?.id ?? 'mod') - debug('projectTypeId:', projectTypeId.value) watch(projectTypeId, (val) => debug('projectTypeId changed:', val)) diff --git a/packages/ui/src/layouts/shared/browse-tab/composables/install-logic.ts b/packages/ui/src/layouts/shared/browse-tab/composables/install-logic.ts index 8a2a52b344..fe39fb2e2b 100644 --- a/packages/ui/src/layouts/shared/browse-tab/composables/install-logic.ts +++ b/packages/ui/src/layouts/shared/browse-tab/composables/install-logic.ts @@ -384,9 +384,26 @@ export function getLoaderFilterTypes(contentType: string) { if (contentType === 'plugin') return ['plugin_loader', 'plugin_platform'] if (contentType === 'modpack') return ['modpack_loader'] if (contentType === 'shader') return ['shader_loader'] + if (contentType === 'datapack') return ['datapack_loader'] return [] } +const SERVER_RUNTIME_INSTALL_FILTER_TYPES = new Set([ + 'game_version', + 'mod_loader', + 'plugin_loader', + 'plugin_platform', + 'datapack_loader', +]) + +export function stripServerRuntimeInstallFilters(filters: readonly FilterValue[]) { + return filters.filter((filter) => !SERVER_RUNTIME_INSTALL_FILTER_TYPES.has(filter.type)) +} + +export function stripServerRuntimeInstallOverrides(filterTypes: readonly string[]) { + return filterTypes.filter((type) => !SERVER_RUNTIME_INSTALL_FILTER_TYPES.has(type)) +} + /** * Merges user-selected filters with target-provided filters for install decisions. * @@ -454,7 +471,12 @@ export function getTargetInstallPreferences( return normalizeInstallPreferences({ gameVersions: gameVersion && shouldUseTargetRuntime ? [gameVersion] : undefined, - loaders: loader && shouldUseTargetRuntime ? [loader] : undefined, + loaders: + contentType === 'datapack' + ? ['datapack'] + : loader && shouldUseTargetRuntime + ? [loader] + : undefined, }) } @@ -515,10 +537,9 @@ export function mergeInstallPreferences( export function getLatestMatchingInstallVersion( versions: readonly Labrinth.Versions.v2.Version[], preferences: BrowseInstallPreferences, - contentType: string, ) { return [...versions] - .filter((version) => versionMatchesPreferences(version, preferences, contentType)) + .filter((version) => versionMatchesPreferences(version, preferences)) .sort((a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime())[0] } @@ -543,11 +564,7 @@ export async function resolveInstallPlan( let lastError: Error | null = null for (const candidate of candidates) { - const version = getLatestMatchingInstallVersion( - versions, - candidate.preferences, - options.contentType, - ) + const version = getLatestMatchingInstallVersion(versions, candidate.preferences) if (version) { const fileName = @@ -747,13 +764,11 @@ function hasPreferences(preferences: BrowseInstallPreferences) { function versionMatchesPreferences( version: Labrinth.Versions.v2.Version, preferences: BrowseInstallPreferences, - contentType: string, ) { const gameVersionMatches = !preferences.gameVersions?.length || version.game_versions.some((gameVersion) => preferences.gameVersions?.includes(gameVersion)) if (!gameVersionMatches) return false - if (contentType === 'datapack') return true if (!preferences.loaders?.length) return true const compatibleLoaders = getCompatibleLoaderAliasSet(preferences.loaders) diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/instances/[instance-id]/content.vue b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/instances/[instance-id]/content.vue index a92e5c7497..5605b9b4c8 100644 --- a/packages/ui/src/layouts/wrapped/hosting/manage/[id]/instances/[instance-id]/content.vue +++ b/packages/ui/src/layouts/wrapped/hosting/manage/[id]/instances/[instance-id]/content.vue @@ -384,7 +384,11 @@ function toResolvePreferences( async function resolveStoredServerAddonPlans(plans: BrowseInstallPlan[]) { const existingProjectIds = getInstalledProjectIds() - const resolvedAddons: Array<{ project_id: string; version_id: string }> = [] + const resolvedAddons: Array<{ + project_id: string + version_id: string + kind: Archon.Content.v1.AddonKind + }> = [] for (const plan of plans) { const target = getTargetInstallPreferences( @@ -409,6 +413,7 @@ async function resolveStoredServerAddonPlans(plans: BrowseInstallPlan[]) { resolvedAddons.push({ project_id: item.project_id, version_id: item.version_id, + kind: plan.contentType as Archon.Content.v1.AddonKind, }) } } @@ -1417,7 +1422,7 @@ provideContentManager({ : (updatingProject?.version?.id ?? '') " :is-app="false" - :project-type="updatingModpack ? 'modpack' : updatingProject?.project_type" + :project-type="updatingModpack ? 'modpack' : type" :project-icon-url=" updatingModpack ? modpack?.project.icon_url : updatingProject?.project?.icon_url " diff --git a/packages/ui/src/utils/version-compatibility.ts b/packages/ui/src/utils/version-compatibility.ts index a2c255bbbc..5006dfe191 100644 --- a/packages/ui/src/utils/version-compatibility.ts +++ b/packages/ui/src/utils/version-compatibility.ts @@ -47,12 +47,16 @@ export function versionMatchesCompatibilityTarget( return false } + const normalizedVersionLoaders = version.loaders.map(normalizeLoaderAlias) + + if (target.projectType === 'datapack') { + return normalizedVersionLoaders.includes('datapack') + } + if (target.projectType && NON_MOD_PROJECT_TYPES.has(target.projectType)) { return true } - const normalizedVersionLoaders = version.loaders.map(normalizeLoaderAlias) - if ( target.projectType === 'modpack' && (normalizedVersionLoaders.length === 0 ||