fix: servers misc fixes (#5475)

* fix: tags in project settings to have icons and ordered correctly

* fix copy in project list layout settings

* fix tag item in header navigation

* adjust ping ranges

* add handle click tag

* fix: dont show offline in project page for draft status

* move tags above creators in app

* preload server project page on load and optimize queries

* add server project card to organization page

* fix minecraft_java_server label

* pnpm prepr

* have user option in project create modal be circle

* feat: implement better mobile project page view

* disable summary line clamp for servers

* fix: unlink instance doesnt update instance

* increase icon upload size

* small fix on button size

* improve how server ping info loads

* remove unnecessary pings for instance page

* fix order of computing dependency diff

* remove linked_project_id from world, use name+address to match for managed world instead

* pnpm prepr

* hide duplicate worlds with same domain name in worlds list

* add install content warning for server instance

* increase summary max width

* add handling for server projects for bulk editing links

* implement include user unlisted projects in published modpack select

* pnpm prepr

* filter to only user unlisted status

* add bad link warnings

* fix modpack tags appearing in server

* cargo fmt
This commit is contained in:
Truman Gao
2026-03-06 18:11:45 -08:00
committed by GitHub
parent 98175a58a6
commit 83d53dafe7
44 changed files with 993 additions and 377 deletions

View File

@@ -2,7 +2,12 @@
<div v-if="instance">
<div class="p-6 pr-2 pb-4" @contextmenu.prevent.stop="(event) => handleRightClick(event)">
<ExportModal ref="exportModal" :instance="instance" />
<InstanceSettingsModal ref="settingsModal" :instance="instance" :offline="offline" />
<InstanceSettingsModal
ref="settingsModal"
:instance="instance"
:offline="offline"
@unlinked="fetchInstance"
/>
<UpdateToPlayModal ref="updateToPlayModal" :instance="instance" />
<ContentPageHeader>
<template #icon>
@@ -55,27 +60,26 @@
</template>
<template v-else>
<ServerOnlinePlayers
v-if="playersOnline !== undefined"
:online="playersOnline"
:status-online="statusOnline"
hide-label
/>
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
<div
v-if="
(playersOnline !== undefined || recentPlays !== undefined) &&
(minecraftServer?.region || ping)
"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
<template v-if="loadingServerPing">
<ServerOnlinePlayers
v-if="playersOnline !== undefined"
:online="playersOnline"
:status-online="statusOnline"
hide-label
/>
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
<div
v-if="
(playersOnline !== undefined || recentPlays !== undefined) &&
(minecraftServer?.region || ping)
"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
<ServerPing v-if="ping" :ping="ping" />
</template>
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
<ServerPing v-if="ping" :ping="ping" />
<div
v-if="minecraftServer?.region || ping"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
@@ -329,7 +333,7 @@ import { get_by_profile_path } from '@/helpers/process'
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
import type { GameInstance } from '@/helpers/types'
import { showProfileInFolder } from '@/helpers/utils.js'
import { get_server_status, getServerLatency } from '@/helpers/worlds'
import { get_server_status } from '@/helpers/worlds'
import { handleSevereError } from '@/store/error.js'
import { playServerProject } from '@/store/install.js'
import { useBreadcrumbs, useLoading } from '@/store/state'
@@ -370,6 +374,7 @@ const recentPlays = computed(
)
const playersOnline = ref<number | undefined>(undefined)
const ping = ref<number | undefined>(undefined)
const loadingServerPing = ref(false)
async function fetchInstance() {
isServerInstance.value = false
@@ -377,6 +382,7 @@ async function fetchInstance() {
modrinthVersions.value = []
ping.value = undefined
playersOnline.value = undefined
loadingServerPing.value = false
instance.value = await get(route.params.id as string).catch(handleError)
@@ -409,14 +415,19 @@ async function fetchInstance() {
function fetchDeferredData() {
const serverAddress = linkedProjectV3.value?.minecraft_java_server?.address
if (isServerInstance.value && serverAddress) {
Promise.all([get_server_status(serverAddress), getServerLatency(serverAddress)])
.then(([status, latency]) => {
ping.value = latency
get_server_status(serverAddress)
.then((status) => {
playersOnline.value = status.players?.online
ping.value = status.ping
})
.catch((err) => {
console.error(`Failed to ping server ${serverAddress}:`, err)
.catch((error) => {
console.error(`Failed to fetch server status for ${serverAddress}:`, error)
})
.finally(() => {
loadingServerPing.value = true
})
} else {
loadingServerPing.value = true
}
updatePlayState()

View File

@@ -26,7 +26,7 @@
:description="`'${worldToDelete?.name}' will be **permanently deleted**, and there will be no way to recover it.`"
@proceed="proceedDeleteWorld"
/>
<div v-if="worlds.length > 0" class="flex flex-col gap-4">
<div v-if="dedupedWorlds.length > 0" class="flex flex-col gap-4">
<div class="flex flex-wrap gap-2 items-center">
<StyledInput
v-model="searchFilter"
@@ -62,6 +62,7 @@
v-for="world in filteredWorlds"
:key="`world-${world.type}-${world.type == 'singleplayer' ? world.path : `${world.address}-${world.index}`}`"
:world="world"
:managed="world.type === 'server' ? isManagedServerWorld(world) : false"
:highlighted="highlightedWorld === getWorldIdentifier(world)"
:supports-server-quick-play="supportsServerQuickPlay"
:supports-world-quick-play="supportsWorldQuickPlay"
@@ -80,13 +81,13 @@
@refresh="() => refreshServer((world as ServerWorld).address)"
@edit="
() =>
isLinkedWorld(world)
? undefined
: world.type === 'server'
? editServerModal?.show(world)
: editWorldModal?.show(world)
world.type === 'singleplayer'
? editWorldModal?.show(world)
: isManagedServerWorld(world)
? undefined
: editServerModal?.show(world)
"
@delete="() => !isLinkedWorld(world) && promptToRemoveWorld(world)"
@delete="() => !isManagedServerWorld(world) && promptToRemoveWorld(world)"
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
/>
</div>
@@ -144,17 +145,19 @@ import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
import EditWorldModal from '@/components/ui/world/modal/EditSingleplayerWorldModal.vue'
import WorldItem from '@/components/ui/world/WorldItem.vue'
import { get_project, get_project_v3 } from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events'
import { get_game_versions } from '@/helpers/tags'
import type { GameInstance } from '@/helpers/types'
import {
delete_world,
get_profile_protocol_version,
getServerDomainKey,
getWorldIdentifier,
handleDefaultProfileUpdateEvent,
hasServerQuickPlaySupport,
hasWorldQuickPlaySupport,
isLinkedWorld,
normalizeServerAddress,
type ProfileEvent,
type ProtocolVersion,
refreshServerData,
@@ -162,6 +165,7 @@ import {
refreshWorld,
refreshWorlds,
remove_server_from_profile,
resolveManagedServerWorld,
type ServerData,
type ServerWorld,
showWorldInFolder,
@@ -171,7 +175,11 @@ import {
start_join_singleplayer_world,
type World,
} from '@/helpers/worlds.ts'
import { playServerProject } from '@/store/install'
import {
ensureManagedServerWorldExists,
getServerAddress,
playServerProject,
} from '@/store/install'
const { handleError } = injectNotificationManager()
const route = useRoute()
@@ -225,6 +233,69 @@ const linuxRefreshCount = ref(0)
const protocolVersion = ref<ProtocolVersion | null>(
await get_profile_protocol_version(instance.value.path),
)
const managedServerName = ref<string | null>(null)
const managedServerAddress = ref<string | null>(null)
const managedServerWorld = computed(() =>
resolveManagedServerWorld(worlds.value, managedServerName.value, managedServerAddress.value),
)
function isManagedServerWorld(world: World): world is ServerWorld {
return world.type === 'server' && managedServerWorld.value?.index === world.index
}
async function refreshManagedServerMetadata() {
await ensureManagedServerWorldExists(
instance.value.path,
managedServerName.value,
managedServerAddress.value,
)
const projectId = instance.value.linked_data?.project_id
if (!projectId) {
managedServerName.value = null
managedServerAddress.value = null
return
}
try {
const [project, projectV3] = await Promise.all([
get_project(projectId, 'bypass'),
get_project_v3(projectId, 'bypass'),
])
if (projectV3?.minecraft_server == null) {
managedServerName.value = null
managedServerAddress.value = null
return
}
const serverAddress = getServerAddress(projectV3.minecraft_java_server)
if (!serverAddress) {
managedServerName.value = null
managedServerAddress.value = null
return
}
managedServerName.value = project.title
managedServerAddress.value = serverAddress
} catch (err) {
console.error(
`Failed to resolve managed server metadata for profile: ${instance.value.path}`,
err,
)
managedServerName.value = null
managedServerAddress.value = null
}
}
watch(
() => instance.value.linked_data?.project_id,
async () => {
await refreshManagedServerMetadata()
},
{ immediate: true },
)
const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
if (e.profile_path_id !== instance.value.path) return
@@ -257,6 +328,7 @@ async function refreshAllWorlds() {
console.log(`Already refreshing, cancelling refresh.`)
return
}
await refreshManagedServerMetadata()
refreshingAll.value = true
@@ -334,8 +406,11 @@ async function joinWorld(world: World) {
startingInstance.value = true
worldPlaying.value = world
if (world.type === 'server') {
if (isLinkedWorld(world)) {
playServerProject(world.linked_project_id)
const managedProjectId = instance.value.linked_data?.project_id
if (managedProjectId && isManagedServerWorld(world)) {
await playServerProject(managedProjectId).catch(handleJoinError)
startingInstance.value = false
return
}
await start_join_server(instance.value.path, world.address).catch(handleJoinError)
} else if (world.type === 'singleplayer') {
@@ -379,12 +454,48 @@ const supportsWorldQuickPlay = computed(() =>
hasWorldQuickPlaySupport(gameVersions.value, instance.value.game_version),
)
const dedupedWorlds = computed(() => {
const visibleWorlds: World[] = []
const serverIndexByDomain = new Map<string, number>()
for (const world of worlds.value) {
if (world.type !== 'server') {
visibleWorlds.push(world)
continue
}
const domainKey =
getServerDomainKey(world.address) ||
normalizeServerAddress(world.address) ||
`server-${world.index}`
const existingIndex = serverIndexByDomain.get(domainKey)
if (existingIndex == null) {
serverIndexByDomain.set(domainKey, visibleWorlds.length)
visibleWorlds.push(world)
continue
}
// replace world with managed world if applicable
const existingWorld = visibleWorlds[existingIndex]
if (
existingWorld?.type === 'server' &&
!isManagedServerWorld(existingWorld) &&
isManagedServerWorld(world)
) {
visibleWorlds[existingIndex] = world
}
}
return visibleWorlds
})
const filterOptions = computed(() => {
const options: FilterBarOption[] = []
const hasServer = worlds.value.some((x) => x.type === 'server')
const hasServer = dedupedWorlds.value.some((x) => x.type === 'server')
if (worlds.value.some((x) => x.type === 'singleplayer') && hasServer) {
if (dedupedWorlds.value.some((x) => x.type === 'singleplayer') && hasServer) {
options.push({
id: 'singleplayer',
message: messages.singleplayer,
@@ -398,13 +509,13 @@ const filterOptions = computed(() => {
if (hasServer) {
// add available filter if there's any offline ("unavailable") servers AND there's any singleplayer worlds or available servers
if (
worlds.value.some(
dedupedWorlds.value.some(
(x) =>
x.type === 'server' &&
!serverData.value[x.address]?.status &&
!serverData.value[x.address]?.refreshing,
) &&
worlds.value.some(
dedupedWorlds.value.some(
(x) =>
x.type === 'singleplayer' ||
(x.type === 'server' &&
@@ -423,7 +534,7 @@ const filterOptions = computed(() => {
})
const filteredWorlds = computed(() =>
worlds.value.filter((x) => {
dedupedWorlds.value.filter((x) => {
const availableFilter = filters.value.includes('available')
const typeFilter = filters.value.includes('server') || filters.value.includes('singleplayer')