Skip to content

Commit 83d53da

Browse files
authored
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
1 parent 98175a5 commit 83d53da

44 files changed

Lines changed: 993 additions & 377 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/app-frontend/src/components/ui/instance_settings/InstallationSettings.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ const modalConfirmUnpair = ref()
4949
const modalConfirmReinstall = ref()
5050
5151
const props = defineProps<InstanceSettingsTabProps>()
52+
const emit = defineEmits<{
53+
unlinked: []
54+
}>()
5255
5356
const loader = ref(props.instance.loader)
5457
const gameVersion = ref(props.instance.game_version)
@@ -273,7 +276,7 @@ async function unpairProfile() {
273276
modpackProject.value = null
274277
modpackVersion.value = null
275278
modpackVersions.value = null
276-
modalConfirmUnpair.value.hide()
279+
emit('unlinked')
277280
}
278281
279282
async function repairModpack() {

apps/app-frontend/src/components/ui/modal/ConfirmModalWrapper.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import { ConfirmModal } from '@modrinth/ui'
3-
import { ref } from 'vue'
3+
import { useTemplateRef } from 'vue'
44
55
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
66
import { useTheming } from '@/store/theme.ts'
@@ -49,16 +49,16 @@ const props = defineProps({
4949
})
5050
5151
const emit = defineEmits(['proceed'])
52-
const modal = ref(null)
52+
const modal = useTemplateRef('modal')
5353
5454
defineExpose({
5555
show: () => {
5656
hide_ads_window()
57-
modal.value.show()
57+
modal.value?.show()
5858
},
5959
hide: () => {
6060
onModalHide()
61-
modal.value.hide()
61+
modal.value?.hide()
6262
},
6363
})
6464

apps/app-frontend/src/components/ui/modal/InstanceSettingsModal.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,12 @@ import type { InstanceSettingsTabProps } from '../../../helpers/types'
3232
const { formatMessage } = useVIntl()
3333
3434
const props = defineProps<InstanceSettingsTabProps>()
35+
const emit = defineEmits<{
36+
unlinked: []
37+
}>()
3538
3639
const isMinecraftServer = ref(false)
40+
const handleUnlinked = () => emit('unlinked')
3741
3842
watch(
3943
() => props.instance,
@@ -121,7 +125,14 @@ defineExpose({ show })
121125

122126
<TabbedModal
123127
:tabs="
124-
tabs.map((tab) => ({ ...tab, props: { ...props, isMinecraftServer: isMinecraftServer } }))
128+
tabs.map((tab) => ({
129+
...tab,
130+
props: {
131+
...props,
132+
isMinecraftServer,
133+
onUnlinked: handleUnlinked,
134+
},
135+
}))
125136
"
126137
/>
127138
</ModalWrapper>

apps/app-frontend/src/components/ui/modal/UpdateToPlayModal.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,11 @@ type ProjectInfo = {
187187
188188
const { formatMessage } = useVIntl()
189189
const installStore = useInstall()
190+
type UpdateCompleteCallback = () => void | Promise<void>
190191
191192
const modal = ref<InstanceType<typeof NewModal>>()
192193
const instance = ref<GameInstance | null>(null)
193-
const onUpdateComplete = ref<() => void>(() => {})
194+
const onUpdateComplete = ref<UpdateCompleteCallback>(() => {})
194195
const diffs = ref<DependencyDiff[]>([])
195196
const modpackVersionId = ref<string | null>(null)
196197
const modpackVersion = ref<Version | null>(null)
@@ -316,6 +317,7 @@ async function computeDependencyDiffs(
316317
317318
async function checkUpdateAvailable(inst: GameInstance): Promise<DependencyDiff[] | null> {
318319
if (!inst.linked_data) return null
320+
if (!modpackVersionId.value || !inst.linked_data.version_id) return null
319321
320322
try {
321323
// For server projects, linked_data.project_id is the server project but
@@ -327,8 +329,8 @@ async function checkUpdateAvailable(inst: GameInstance): Promise<DependencyDiff[
327329
// Compute dependency diffs between current and latest version
328330
if (instanceModpackVersion && modpackVersion.value) {
329331
return await computeDependencyDiffs(
330-
modpackVersion.value.dependencies || [],
331332
instanceModpackVersion.dependencies || [],
333+
modpackVersion.value.dependencies || [],
332334
)
333335
}
334336
} catch (error) {
@@ -355,7 +357,7 @@ async function handleUpdate() {
355357
try {
356358
if (modpackVersionId.value && instance.value) {
357359
await update_managed_modrinth_version(instance.value.path, modpackVersionId.value)
358-
onUpdateComplete.value()
360+
await onUpdateComplete.value()
359361
}
360362
} catch (error) {
361363
console.error('Error updating instance:', error)
@@ -379,7 +381,7 @@ function handleDecline() {
379381
function show(
380382
instanceVal: GameInstance,
381383
modpackVersionIdVal: string | null = null,
382-
callback: () => void = () => {},
384+
callback: UpdateCompleteCallback = () => {},
383385
e?: MouseEvent,
384386
) {
385387
instance.value = instanceVal

apps/app-frontend/src/components/ui/world/WorldItem.vue

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import type {
4545
SingleplayerWorld,
4646
World,
4747
} from '@/helpers/worlds.ts'
48-
import { getWorldIdentifier, isLinkedWorld, set_world_display_status } from '@/helpers/worlds.ts'
48+
import { getWorldIdentifier, set_world_display_status } from '@/helpers/worlds.ts'
4949
5050
import { LockIcon } from '../../../../../../packages/assets/generated-icons'
5151
@@ -81,6 +81,8 @@ const props = withDefaults(
8181
message: MessageDescriptor
8282
}
8383
84+
managed?: boolean
85+
8486
// Instance
8587
instancePath?: string
8688
instanceName?: string
@@ -99,6 +101,7 @@ const props = withDefaults(
99101
renderedMotd: undefined,
100102
101103
gameMode: undefined,
104+
managed: false,
102105
103106
instancePath: undefined,
104107
instanceName: undefined,
@@ -120,7 +123,7 @@ const serverIncompatible = computed(
120123
)
121124
122125
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
123-
const linked = computed(() => isLinkedWorld(props.world))
126+
const managed = computed(() => props.managed)
124127
125128
const messages = defineMessages({
126129
hardcore: {
@@ -209,7 +212,7 @@ const messages = defineMessages({
209212
{{ world.name }}
210213
</div>
211214
<TagItem
212-
v-if="linked"
215+
v-if="managed"
213216
v-tooltip="formatMessage(messages.linkedServer)"
214217
class="border !border-solid border-blue bg-highlight-blue text-xs"
215218
:style="`--_color: var(--color-blue)`"
@@ -412,10 +415,10 @@ const messages = defineMessages({
412415
id: 'edit',
413416
action: () => emit('edit'),
414417
shown: !instancePath,
415-
disabled: locked || linked,
418+
disabled: locked || managed,
416419
tooltip: locked
417420
? formatMessage(messages.worldInUse)
418-
: linked
421+
: managed
419422
? formatMessage(messages.linkedServer)
420423
: undefined,
421424
},
@@ -452,10 +455,10 @@ const messages = defineMessages({
452455
hoverFilled: true,
453456
action: () => emit('delete'),
454457
shown: !instancePath,
455-
disabled: locked || linked,
458+
disabled: locked || managed,
456459
tooltip: locked
457460
? formatMessage(messages.worldInUse)
458-
: linked
461+
: managed
459462
? formatMessage(messages.linkedServer)
460463
: undefined,
461464
},

apps/app-frontend/src/helpers/worlds.ts

Lines changed: 123 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export type ServerWorld = BaseWorld & {
3030
index: number
3131
address: string
3232
pack_status: ServerPackStatus
33-
linked_project_id?: string
3433
}
3534

3635
export type World = SingleplayerWorld | ServerWorld
@@ -141,14 +140,12 @@ export async function add_server_to_profile(
141140
name: string,
142141
address: string,
143142
packStatus: ServerPackStatus,
144-
linkedProjectId?: string,
145143
): Promise<number> {
146144
return await invoke('plugin:worlds|add_server_to_profile', {
147145
path,
148146
name,
149147
address,
150148
packStatus,
151-
linkedProjectId,
152149
})
153150
}
154151

@@ -158,15 +155,13 @@ export async function edit_server_in_profile(
158155
name: string,
159156
address: string,
160157
packStatus: ServerPackStatus,
161-
linkedProjectId?: string,
162158
): Promise<void> {
163159
return await invoke('plugin:worlds|edit_server_in_profile', {
164160
path,
165161
index,
166162
name,
167163
address,
168164
packStatus,
169-
linkedProjectId,
170165
})
171166
}
172167

@@ -204,11 +199,6 @@ export function getWorldIdentifier(world: World) {
204199

205200
export function sortWorlds(worlds: World[]) {
206201
worlds.sort((a, b) => {
207-
const aLinked = isLinkedWorld(a)
208-
const bLinked = isLinkedWorld(b)
209-
if (aLinked !== bLinked) {
210-
return aLinked ? -1 : 1
211-
}
212202
if (!a.last_played) {
213203
return 1
214204
}
@@ -227,8 +217,129 @@ export function isServerWorld(world: World): world is ServerWorld {
227217
return world.type === 'server'
228218
}
229219

230-
export function isLinkedWorld(world: World): boolean {
231-
return world.type === 'server' && !!world.linked_project_id
220+
const DEFAULT_MINECRAFT_SERVER_PORT = 25565
221+
222+
function parseServerPort(port: string): number | null {
223+
const parsed = Number.parseInt(port, 10)
224+
return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : null
225+
}
226+
227+
function parseServerHost(address: string): string {
228+
const trimmedAddress = address.trim()
229+
if (!trimmedAddress) return ''
230+
231+
if (trimmedAddress.startsWith('[')) {
232+
const closingBracket = trimmedAddress.indexOf(']')
233+
if (closingBracket > 0) {
234+
return trimmedAddress.slice(1, closingBracket).trim().toLowerCase()
235+
}
236+
}
237+
238+
const firstColon = trimmedAddress.indexOf(':')
239+
const lastColon = trimmedAddress.lastIndexOf(':')
240+
241+
if (firstColon !== -1 && firstColon === lastColon) {
242+
return trimmedAddress.slice(0, firstColon).trim().toLowerCase()
243+
}
244+
245+
return trimmedAddress.toLowerCase()
246+
}
247+
248+
function isIPv4Host(host: string): boolean {
249+
const segments = host.split('.')
250+
if (segments.length !== 4) return false
251+
252+
return segments.every((segment) => {
253+
if (!/^\d+$/.test(segment)) return false
254+
const value = Number.parseInt(segment, 10)
255+
return value >= 0 && value <= 255
256+
})
257+
}
258+
259+
/**
260+
* Normalization converts addresses to a canonical form (lowercase-host:port, default port 25565)
261+
*/
262+
export function normalizeServerAddress(address: string): string {
263+
const trimmedAddress = address.trim()
264+
const host = parseServerHost(trimmedAddress)
265+
if (!host) return ''
266+
let port = DEFAULT_MINECRAFT_SERVER_PORT
267+
268+
// ipv6 address
269+
if (trimmedAddress.startsWith('[')) {
270+
const closingBracket = trimmedAddress.indexOf(']')
271+
if (closingBracket > 0) {
272+
const suffix = trimmedAddress.slice(closingBracket + 1)
273+
if (suffix.startsWith(':')) {
274+
const parsedPort = parseServerPort(suffix.slice(1))
275+
if (parsedPort != null) {
276+
port = parsedPort
277+
}
278+
}
279+
}
280+
281+
// ipv4 address or hostname
282+
} else {
283+
const firstColon = trimmedAddress.indexOf(':')
284+
const lastColon = trimmedAddress.lastIndexOf(':')
285+
if (firstColon !== -1 && firstColon === lastColon) {
286+
const parsedPort = parseServerPort(trimmedAddress.slice(firstColon + 1))
287+
if (parsedPort != null) {
288+
port = parsedPort
289+
}
290+
}
291+
}
292+
293+
return `${host}:${port}`
294+
}
295+
296+
/**
297+
* Domain key used for deduping server entries by removing a single leading subdomain.
298+
* Example: test.cobblemon.gg and cobblemon.gg map to cobblemon.gg
299+
*/
300+
export function getServerDomainKey(address: string): string {
301+
const normalizedAddress = normalizeServerAddress(address)
302+
if (!normalizedAddress) return ''
303+
304+
const separator = normalizedAddress.lastIndexOf(':')
305+
if (separator <= 0 || separator === normalizedAddress.length - 1) return normalizedAddress
306+
307+
const host = normalizedAddress.slice(0, separator).replace(/\.+$/, '')
308+
if (!host) return normalizedAddress
309+
if (host.includes(':') || isIPv4Host(host)) return normalizedAddress
310+
311+
const segments = host.split('.').filter(Boolean)
312+
if (segments.length <= 2) return host
313+
314+
return segments.slice(1).join('.')
315+
}
316+
317+
export function resolveManagedServerWorld(
318+
worlds: World[],
319+
managedName: string | null | undefined,
320+
managedAddress: string | null | undefined,
321+
): ServerWorld | null {
322+
if (!managedName || !managedAddress) return null
323+
324+
const normalizedManagedAddress = normalizeServerAddress(managedAddress)
325+
if (!normalizedManagedAddress) return null
326+
327+
const servers = worlds
328+
.filter(isServerWorld)
329+
.slice()
330+
.sort((a, b) => a.index - b.index)
331+
332+
const exactMatch = servers.find(
333+
(server) =>
334+
server.name === managedName &&
335+
normalizeServerAddress(server.address) === normalizedManagedAddress,
336+
)
337+
if (exactMatch) return exactMatch
338+
339+
return (
340+
servers.find((server) => normalizeServerAddress(server.address) === normalizedManagedAddress) ??
341+
null
342+
)
232343
}
233344

234345
export async function getServerLatency(

apps/app-frontend/src/pages/Browse.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '@modrinth/assets'
1212
import type { ProjectType, SortType, Tags } from '@modrinth/ui'
1313
import {
14+
Admonition,
1415
ButtonStyled,
1516
Checkbox,
1617
defineMessages,
@@ -724,6 +725,10 @@ previousFilterState.value = JSON.stringify({
724725
<template v-if="instance">
725726
<InstanceIndicator :instance="instance" />
726727
<h1 class="m-0 mb-1 text-xl">Install content to instance</h1>
728+
<Admonition v-if="isServerInstance" type="warning" class="mb-1">
729+
Adding content can break compatibility when joining the server. Any added content will also
730+
be lost when you update the server instance content.
731+
</Admonition>
727732
</template>
728733
<NavTabs :links="selectableProjectTypes" />
729734
<StyledInput

0 commit comments

Comments
 (0)