Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
624ad62
fix: tags in project settings to have icons and ordered correctly
tdgao Mar 4, 2026
e7d4066
fix copy in project list layout settings
tdgao Mar 4, 2026
7289801
fix tag item in header navigation
tdgao Mar 4, 2026
ad17781
adjust ping ranges
tdgao Mar 4, 2026
573cf82
add handle click tag
tdgao Mar 4, 2026
978c45b
fix: dont show offline in project page for draft status
tdgao Mar 4, 2026
cefb057
move tags above creators in app
tdgao Mar 4, 2026
9deff5b
preload server project page on load and optimize queries
tdgao Mar 5, 2026
72cf5aa
add server project card to organization page
tdgao Mar 5, 2026
e358b66
fix minecraft_java_server label
tdgao Mar 5, 2026
67e6dc0
pnpm prepr
tdgao Mar 5, 2026
5ad5bbe
have user option in project create modal be circle
tdgao Mar 5, 2026
79914a7
feat: implement better mobile project page view
tdgao Mar 5, 2026
795d61d
disable summary line clamp for servers
tdgao Mar 5, 2026
1a17aa9
fix: unlink instance doesnt update instance
tdgao Mar 5, 2026
bad5e5d
increase icon upload size
tdgao Mar 5, 2026
e8fecb9
small fix on button size
tdgao Mar 5, 2026
7d38a77
improve how server ping info loads
tdgao Mar 5, 2026
70e8187
remove unnecessary pings for instance page
tdgao Mar 5, 2026
b988f51
fix order of computing dependency diff
tdgao Mar 5, 2026
991b040
remove linked_project_id from world, use name+address to match for ma…
tdgao Mar 5, 2026
b858e4b
pnpm prepr
tdgao Mar 5, 2026
b9b0451
hide duplicate worlds with same domain name in worlds list
tdgao Mar 5, 2026
974b254
add install content warning for server instance
tdgao Mar 6, 2026
1161ac4
increase summary max width
tdgao Mar 6, 2026
89eb4bb
add handling for server projects for bulk editing links
tdgao Mar 6, 2026
f6e4356
implement include user unlisted projects in published modpack select
tdgao Mar 6, 2026
5b635b1
pnpm prepr
tdgao Mar 6, 2026
b8ba067
filter to only user unlisted status
tdgao Mar 6, 2026
16d8c07
add bad link warnings
tdgao Mar 6, 2026
37763a2
fix modpack tags appearing in server
tdgao Mar 6, 2026
8d5853d
cargo fmt
tdgao Mar 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ const modalConfirmUnpair = ref()
const modalConfirmReinstall = ref()

const props = defineProps<InstanceSettingsTabProps>()
const emit = defineEmits<{
unlinked: []
}>()

const loader = ref(props.instance.loader)
const gameVersion = ref(props.instance.game_version)
Expand Down Expand Up @@ -273,7 +276,7 @@ async function unpairProfile() {
modpackProject.value = null
modpackVersion.value = null
modpackVersions.value = null
modalConfirmUnpair.value.hide()
emit('unlinked')
}

async function repairModpack() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ConfirmModal } from '@modrinth/ui'
import { ref } from 'vue'
import { useTemplateRef } from 'vue'

import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
import { useTheming } from '@/store/theme.ts'
Expand Down Expand Up @@ -49,16 +49,16 @@ const props = defineProps({
})

const emit = defineEmits(['proceed'])
const modal = ref(null)
const modal = useTemplateRef('modal')

defineExpose({
show: () => {
hide_ads_window()
modal.value.show()
modal.value?.show()
},
hide: () => {
onModalHide()
modal.value.hide()
modal.value?.hide()
},
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ import type { InstanceSettingsTabProps } from '../../../helpers/types'
const { formatMessage } = useVIntl()

const props = defineProps<InstanceSettingsTabProps>()
const emit = defineEmits<{
unlinked: []
}>()

const isMinecraftServer = ref(false)
const handleUnlinked = () => emit('unlinked')

watch(
() => props.instance,
Expand Down Expand Up @@ -121,7 +125,14 @@ defineExpose({ show })

<TabbedModal
:tabs="
tabs.map((tab) => ({ ...tab, props: { ...props, isMinecraftServer: isMinecraftServer } }))
tabs.map((tab) => ({
...tab,
props: {
...props,
isMinecraftServer,
onUnlinked: handleUnlinked,
},
}))
"
/>
</ModalWrapper>
Expand Down
10 changes: 6 additions & 4 deletions apps/app-frontend/src/components/ui/modal/UpdateToPlayModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,11 @@ type ProjectInfo = {

const { formatMessage } = useVIntl()
const installStore = useInstall()
type UpdateCompleteCallback = () => void | Promise<void>

const modal = ref<InstanceType<typeof NewModal>>()
const instance = ref<GameInstance | null>(null)
const onUpdateComplete = ref<() => void>(() => {})
const onUpdateComplete = ref<UpdateCompleteCallback>(() => {})
const diffs = ref<DependencyDiff[]>([])
const modpackVersionId = ref<string | null>(null)
const modpackVersion = ref<Version | null>(null)
Expand Down Expand Up @@ -316,6 +317,7 @@ async function computeDependencyDiffs(

async function checkUpdateAvailable(inst: GameInstance): Promise<DependencyDiff[] | null> {
if (!inst.linked_data) return null
if (!modpackVersionId.value || !inst.linked_data.version_id) return null

try {
// For server projects, linked_data.project_id is the server project but
Expand All @@ -327,8 +329,8 @@ async function checkUpdateAvailable(inst: GameInstance): Promise<DependencyDiff[
// Compute dependency diffs between current and latest version
if (instanceModpackVersion && modpackVersion.value) {
return await computeDependencyDiffs(
modpackVersion.value.dependencies || [],
instanceModpackVersion.dependencies || [],
modpackVersion.value.dependencies || [],
)
}
} catch (error) {
Expand All @@ -355,7 +357,7 @@ async function handleUpdate() {
try {
if (modpackVersionId.value && instance.value) {
await update_managed_modrinth_version(instance.value.path, modpackVersionId.value)
onUpdateComplete.value()
await onUpdateComplete.value()
}
} catch (error) {
console.error('Error updating instance:', error)
Expand All @@ -379,7 +381,7 @@ function handleDecline() {
function show(
instanceVal: GameInstance,
modpackVersionIdVal: string | null = null,
callback: () => void = () => {},
callback: UpdateCompleteCallback = () => {},
e?: MouseEvent,
) {
instance.value = instanceVal
Expand Down
17 changes: 10 additions & 7 deletions apps/app-frontend/src/components/ui/world/WorldItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import type {
SingleplayerWorld,
World,
} from '@/helpers/worlds.ts'
import { getWorldIdentifier, isLinkedWorld, set_world_display_status } from '@/helpers/worlds.ts'
import { getWorldIdentifier, set_world_display_status } from '@/helpers/worlds.ts'
import { LockIcon } from '../../../../../../packages/assets/generated-icons'
Expand Down Expand Up @@ -81,6 +81,8 @@ const props = withDefaults(
message: MessageDescriptor
}
managed?: boolean
// Instance
instancePath?: string
instanceName?: string
Expand All @@ -99,6 +101,7 @@ const props = withDefaults(
renderedMotd: undefined,
gameMode: undefined,
managed: false,
instancePath: undefined,
instanceName: undefined,
Expand All @@ -120,7 +123,7 @@ const serverIncompatible = computed(
)
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
const linked = computed(() => isLinkedWorld(props.world))
const managed = computed(() => props.managed)
const messages = defineMessages({
hardcore: {
Expand Down Expand Up @@ -209,7 +212,7 @@ const messages = defineMessages({
{{ world.name }}
</div>
<TagItem
v-if="linked"
v-if="managed"
v-tooltip="formatMessage(messages.linkedServer)"
class="border !border-solid border-blue bg-highlight-blue text-xs"
:style="`--_color: var(--color-blue)`"
Expand Down Expand Up @@ -412,10 +415,10 @@ const messages = defineMessages({
id: 'edit',
action: () => emit('edit'),
shown: !instancePath,
disabled: locked || linked,
disabled: locked || managed,
tooltip: locked
? formatMessage(messages.worldInUse)
: linked
: managed
? formatMessage(messages.linkedServer)
: undefined,
},
Expand Down Expand Up @@ -452,10 +455,10 @@ const messages = defineMessages({
hoverFilled: true,
action: () => emit('delete'),
shown: !instancePath,
disabled: locked || linked,
disabled: locked || managed,
tooltip: locked
? formatMessage(messages.worldInUse)
: linked
: managed
? formatMessage(messages.linkedServer)
: undefined,
},
Expand Down
135 changes: 123 additions & 12 deletions apps/app-frontend/src/helpers/worlds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export type ServerWorld = BaseWorld & {
index: number
address: string
pack_status: ServerPackStatus
linked_project_id?: string
}

export type World = SingleplayerWorld | ServerWorld
Expand Down Expand Up @@ -141,14 +140,12 @@ export async function add_server_to_profile(
name: string,
address: string,
packStatus: ServerPackStatus,
linkedProjectId?: string,
): Promise<number> {
return await invoke('plugin:worlds|add_server_to_profile', {
path,
name,
address,
packStatus,
linkedProjectId,
})
}

Expand All @@ -158,15 +155,13 @@ export async function edit_server_in_profile(
name: string,
address: string,
packStatus: ServerPackStatus,
linkedProjectId?: string,
): Promise<void> {
return await invoke('plugin:worlds|edit_server_in_profile', {
path,
index,
name,
address,
packStatus,
linkedProjectId,
})
}

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

export function sortWorlds(worlds: World[]) {
worlds.sort((a, b) => {
const aLinked = isLinkedWorld(a)
const bLinked = isLinkedWorld(b)
if (aLinked !== bLinked) {
return aLinked ? -1 : 1
}
if (!a.last_played) {
return 1
}
Expand All @@ -227,8 +217,129 @@ export function isServerWorld(world: World): world is ServerWorld {
return world.type === 'server'
}

export function isLinkedWorld(world: World): boolean {
return world.type === 'server' && !!world.linked_project_id
const DEFAULT_MINECRAFT_SERVER_PORT = 25565

function parseServerPort(port: string): number | null {
const parsed = Number.parseInt(port, 10)
return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : null
}

function parseServerHost(address: string): string {
const trimmedAddress = address.trim()
if (!trimmedAddress) return ''

if (trimmedAddress.startsWith('[')) {
const closingBracket = trimmedAddress.indexOf(']')
if (closingBracket > 0) {
return trimmedAddress.slice(1, closingBracket).trim().toLowerCase()
}
}

const firstColon = trimmedAddress.indexOf(':')
const lastColon = trimmedAddress.lastIndexOf(':')

if (firstColon !== -1 && firstColon === lastColon) {
return trimmedAddress.slice(0, firstColon).trim().toLowerCase()
}

return trimmedAddress.toLowerCase()
}

function isIPv4Host(host: string): boolean {
const segments = host.split('.')
if (segments.length !== 4) return false

return segments.every((segment) => {
if (!/^\d+$/.test(segment)) return false
const value = Number.parseInt(segment, 10)
return value >= 0 && value <= 255
})
}

/**
* Normalization converts addresses to a canonical form (lowercase-host:port, default port 25565)
*/
export function normalizeServerAddress(address: string): string {
const trimmedAddress = address.trim()
const host = parseServerHost(trimmedAddress)
if (!host) return ''
let port = DEFAULT_MINECRAFT_SERVER_PORT

// ipv6 address
if (trimmedAddress.startsWith('[')) {
const closingBracket = trimmedAddress.indexOf(']')
if (closingBracket > 0) {
const suffix = trimmedAddress.slice(closingBracket + 1)
if (suffix.startsWith(':')) {
const parsedPort = parseServerPort(suffix.slice(1))
if (parsedPort != null) {
port = parsedPort
}
}
}

// ipv4 address or hostname
} else {
const firstColon = trimmedAddress.indexOf(':')
const lastColon = trimmedAddress.lastIndexOf(':')
if (firstColon !== -1 && firstColon === lastColon) {
const parsedPort = parseServerPort(trimmedAddress.slice(firstColon + 1))
if (parsedPort != null) {
port = parsedPort
}
}
}

return `${host}:${port}`
}

/**
* Domain key used for deduping server entries by removing a single leading subdomain.
* Example: test.cobblemon.gg and cobblemon.gg map to cobblemon.gg
*/
export function getServerDomainKey(address: string): string {
const normalizedAddress = normalizeServerAddress(address)
if (!normalizedAddress) return ''

const separator = normalizedAddress.lastIndexOf(':')
if (separator <= 0 || separator === normalizedAddress.length - 1) return normalizedAddress

const host = normalizedAddress.slice(0, separator).replace(/\.+$/, '')
if (!host) return normalizedAddress
if (host.includes(':') || isIPv4Host(host)) return normalizedAddress

const segments = host.split('.').filter(Boolean)
if (segments.length <= 2) return host

return segments.slice(1).join('.')
}

export function resolveManagedServerWorld(
worlds: World[],
managedName: string | null | undefined,
managedAddress: string | null | undefined,
): ServerWorld | null {
if (!managedName || !managedAddress) return null

const normalizedManagedAddress = normalizeServerAddress(managedAddress)
if (!normalizedManagedAddress) return null

const servers = worlds
.filter(isServerWorld)
.slice()
.sort((a, b) => a.index - b.index)

const exactMatch = servers.find(
(server) =>
server.name === managedName &&
normalizeServerAddress(server.address) === normalizedManagedAddress,
)
if (exactMatch) return exactMatch

return (
servers.find((server) => normalizeServerAddress(server.address) === normalizedManagedAddress) ??
null
)
}

export async function getServerLatency(
Expand Down
5 changes: 5 additions & 0 deletions apps/app-frontend/src/pages/Browse.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@modrinth/assets'
import type { ProjectType, SortType, Tags } from '@modrinth/ui'
import {
Admonition,
ButtonStyled,
Checkbox,
defineMessages,
Expand Down Expand Up @@ -724,6 +725,10 @@ previousFilterState.value = JSON.stringify({
<template v-if="instance">
<InstanceIndicator :instance="instance" />
<h1 class="m-0 mb-1 text-xl">Install content to instance</h1>
<Admonition v-if="isServerInstance" type="warning" class="mb-1">
Adding content can break compatibility when joining the server. Any added content will also
be lost when you update the server instance content.
</Admonition>
</template>
<NavTabs :links="selectableProjectTypes" />
<StyledInput
Expand Down
Loading