Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
567acaf
feat: world switching page implementation
IMB11 Apr 27, 2026
dced764
fix: lint + i18n
IMB11 Apr 27, 2026
a78b29a
feat: world card alignment
IMB11 Apr 27, 2026
4bec77d
feat: reshuffle layout for worlds
IMB11 Apr 27, 2026
c2cfc1e
chore: clean up wrapped layout folder structure
IMB11 Apr 27, 2026
f38c54f
fix: lint
IMB11 Apr 27, 2026
34f73d3
feat: fix btn sizing
IMB11 Apr 30, 2026
8876128
fix: worlds layout
IMB11 Apr 30, 2026
00b0d40
fix: modpack linked text alignment
IMB11 Apr 30, 2026
23d93da
chore: rename worlds -> instances
IMB11 May 18, 2026
f1d575f
qa: pass
IMB11 May 18, 2026
651214e
fix: lint
IMB11 May 18, 2026
4e425ad
fix: header issues
IMB11 May 18, 2026
b93b104
feat: PageHeader migration start
IMB11 May 18, 2026
07ae486
feat: header migration pt 2
IMB11 May 18, 2026
b03e9bb
fix: header power state
IMB11 May 18, 2026
04e16bf
fix: ssr
IMB11 May 18, 2026
f353a85
fix: ssr for instances subpages with middleware
IMB11 May 18, 2026
2e517ee
feat: migrate all headers to use PageHeader shared component.
IMB11 May 18, 2026
638a109
feat: qa + app routing bugs
IMB11 May 23, 2026
9161e11
feat: qa
IMB11 May 23, 2026
5f36589
fix: project middleware startLoading
IMB11 May 23, 2026
3b41dfc
feat: server settings split up + copy changes across panel warning mo…
IMB11 May 23, 2026
6e6dadd
fix: lint
IMB11 May 23, 2026
ab7e51a
fix: loss of isNuxt
IMB11 Jun 5, 2026
4c45515
feat: switch world route impl
IMB11 Jun 9, 2026
740bf77
feat: implement world creation flow routes
IMB11 Jun 10, 2026
6131c16
fix: qa + lint + i18n
IMB11 Jun 10, 2026
a47ed9c
feat: impl world scoped power endpoint
IMB11 Jun 11, 2026
89e83ab
feat: impl delete world + move onboarding reset to server scope
IMB11 Jun 12, 2026
0db6cff
fix: reset to onboarding flow not waiting
IMB11 Jun 13, 2026
0aa7af9
feat: improve onboarding flow wait logic
IMB11 Jun 13, 2026
72bc5ff
fix: browse page not respecting instance source via wid param
IMB11 Jun 13, 2026
f9833cd
fix: better handling of datapacks in frontend & desync issues between…
IMB11 Jun 13, 2026
6501770
fix: lint
IMB11 Jun 13, 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
80 changes: 76 additions & 4 deletions apps/app-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ const APP_LEFT_NAV_WIDTH = '4rem'
const APP_SIDEBAR_WIDTH = 300
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime()
const ROUTE_SUSPENSE_TIMEOUT_MS = 60_000
const credentials = ref()
const sidebarToggled = ref(true)
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
Expand All @@ -154,6 +155,22 @@ const forceSidebar = computed(
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const keepAliveRouteComponents = computed(() => [
...new Set(
router
.getRoutes()
.map((route) => route.meta.keepAliveComponent)
.filter((name) => typeof name === 'string'),
),
])

function getRouteViewKey(viewRoute) {
const keepAliveKey = viewRoute.meta.keepAliveKey
if (typeof keepAliveKey === 'function') return keepAliveKey(viewRoute)
if (typeof keepAliveKey === 'string') return keepAliveKey
return undefined
}

const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
const prideFundraiserEnabled = computed(
() => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
Expand Down Expand Up @@ -494,6 +511,11 @@ const sidebarOverlayScrollbarsOptions = Object.freeze({
},
})

router.beforeEach(async (to) => {
const redirect = await resolveLegacyServerInstanceTabRedirect(to)
if (redirect) return redirect
})

router.beforeEach(() => {
suspensePending = false
if (routerToken) loading.end(routerToken)
Expand Down Expand Up @@ -525,6 +547,50 @@ function onSuspensePending() {
suspenseToken = loading.begin()
}

async function resolveLegacyServerInstanceTabRedirect(to) {
if (!['ServerManageContent', 'ServerManageFiles', 'ServerManageBackups'].includes(to.name)) {
return null
}

const serverId = getRouteParam(to.params.id)
if (!serverId) return null

const tabPath =
to.name === 'ServerManageFiles' ? '/files' : to.name === 'ServerManageBackups' ? '/backups' : ''
const instancesPath = `/hosting/manage/${encodeURIComponent(serverId)}/instances`

try {
const serverFull = await tauriApiClient.archon.servers_v1.get(serverId)
const world = serverFull.worlds.find((item) => item.is_active) ?? serverFull.worlds[0]
if (world) {
return {
path: `${instancesPath}/${encodeURIComponent(world.id)}${tabPath}`,
query: to.query,
hash: to.hash,
replace: true,
}
}
} catch {
return {
path: instancesPath,
query: to.query,
hash: to.hash,
replace: true,
}
}

return {
path: instancesPath,
query: to.query,
hash: to.hash,
replace: true,
}
}

function getRouteParam(param) {
return Array.isArray(param) ? param[0] : param
}

function onSuspenseResolve() {
if (suspenseToken) {
loading.end(suspenseToken)
Expand Down Expand Up @@ -1607,11 +1673,17 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
>
{{ formatMessage(messages.authUnreachableBody) }}
</Admonition>
<RouterView v-slot="{ Component }">
<RouterView v-slot="{ Component, route: viewRoute }">
<template v-if="Component">
<Suspense @pending="onSuspensePending" @resolve="onSuspenseResolve">
<component :is="Component"></component>
</Suspense>
<KeepAlive :include="keepAliveRouteComponents" :max="3">
<Suspense
:timeout="ROUTE_SUSPENSE_TIMEOUT_MS"
@pending="onSuspensePending"
@resolve="onSuspenseResolve"
>
<component :is="Component" :key="getRouteViewKey(viewRoute)"></component>
</Suspense>
</KeepAlive>
</template>
</RouterView>
</div>
Expand Down
51 changes: 21 additions & 30 deletions apps/app-frontend/src/composables/browse/use-app-server-browse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import type { Labrinth } from '@modrinth/api-client'
import { CheckIcon, PlayIcon, PlusIcon, StopCircleIcon } from '@modrinth/assets'
import type { CardAction } from '@modrinth/ui'
import { commonMessages, defineMessages, useDebugLogger, useVIntl } from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
import type { ComputedRef, Ref } from 'vue'
import { onUnmounted, ref, shallowRef } from 'vue'
import type { Router } from 'vue-router'

import {
fetchCachedServerStatus,
getFreshCachedServerStatus,
} from '@/composables/instances/use-server-status-query'
import { process_listener } from '@/helpers/events'
import { get_by_profile_path } from '@/helpers/process'
import { kill, list as listInstances } from '@/helpers/profile.js'
import type { GameInstance } from '@/helpers/types'
import { add_server_to_profile, getServerLatency } from '@/helpers/worlds'
import { add_server_to_profile } from '@/helpers/worlds'
import { getServerAddress } from '@/store/install.js'

interface BrowseServerInstance {
Expand Down Expand Up @@ -65,14 +70,13 @@ const messages = defineMessages({

export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const debugLog = useDebugLogger('BrowseServer')
const serverPings = shallowRef<Record<string, number | undefined>>({})
const serverPingCache = new Map<string, number | undefined>()
const pendingServerPings = new Map<string, Promise<number | undefined>>()
const runningServerProjects = ref<Record<string, string>>({})
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
const contextMenuRef = ref<ContextMenuHandle | null>(null)
let serverPingCacheActive = true
let serverPingsActive = true
let unlistenProcesses: (() => void) | null = null

async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
Expand Down Expand Up @@ -145,37 +149,26 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
})
const nextPings = { ...serverPings.value }
for (const { hit, address } of pingsToFetch) {
if (serverPingCache.has(address)) {
nextPings[hit.project_id] = serverPingCache.get(address)
const cachedStatus = getFreshCachedServerStatus(queryClient, address)
if (cachedStatus) {
nextPings[hit.project_id] = cachedStatus.ping
}
}
serverPings.value = nextPings

await Promise.all(
pingsToFetch.map(async ({ hit, address }) => {
if (serverPingCache.has(address)) return
if (getFreshCachedServerStatus(queryClient, address)) return

let pending = pendingServerPings.get(address)
if (!pending) {
pending = getServerLatency(address)
.then((latency) => {
if (serverPingCacheActive) serverPingCache.set(address, latency)
return latency
})
.catch((error) => {
console.error(`Failed to ping server ${address}:`, error)
if (serverPingCacheActive) serverPingCache.set(address, undefined)
return undefined
})
.finally(() => {
pendingServerPings.delete(address)
})
pendingServerPings.set(address, pending)
try {
const status = await fetchCachedServerStatus(queryClient, address)
if (!serverPingsActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: status.ping }
} catch (error) {
console.error(`Failed to ping server ${address}:`, error)
if (!serverPingsActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: undefined }
}

const latency = await pending
if (!serverPingCacheActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
}),
)
}
Expand Down Expand Up @@ -307,10 +300,8 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
.catch(options.handleError)

onUnmounted(() => {
serverPingCacheActive = false
serverPingsActive = false
unlistenProcesses?.()
serverPingCache.clear()
pendingServerPings.clear()
})

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { QueryClient } from '@tanstack/vue-query'

import {
get_server_status,
normalizeServerAddress,
type ProtocolVersion,
type ServerStatus,
} from '@/helpers/worlds'

export const SERVER_STATUS_CACHE_MS = 10 * 60 * 1000

function getProtocolVersionKey(protocolVersion: ProtocolVersion | null) {
if (!protocolVersion) return 'default'
return `${protocolVersion.version}:${protocolVersion.legacy ? 'legacy' : 'modern'}`
}

export function getServerStatusQueryKey(
address: string,
protocolVersion: ProtocolVersion | null = null,
) {
return [
'minecraft-server-status',
normalizeServerAddress(address) || address.trim().toLowerCase(),
getProtocolVersionKey(protocolVersion),
] as const
}

export function getFreshCachedServerStatus(
queryClient: QueryClient,
address: string,
protocolVersion: ProtocolVersion | null = null,
) {
const queryKey = getServerStatusQueryKey(address, protocolVersion)
const updatedAt = queryClient.getQueryState(queryKey)?.dataUpdatedAt ?? 0
if (!updatedAt || Date.now() - updatedAt >= SERVER_STATUS_CACHE_MS) return undefined
return queryClient.getQueryData<ServerStatus>(queryKey)
}

export async function fetchCachedServerStatus(
queryClient: QueryClient,
address: string,
protocolVersion: ProtocolVersion | null = null,
) {
return await queryClient.fetchQuery({
queryKey: getServerStatusQueryKey(address, protocolVersion),
queryFn: () => get_server_status(address, protocolVersion),
staleTime: SERVER_STATUS_CACHE_MS,
gcTime: SERVER_STATUS_CACHE_MS,
})
}
6 changes: 6 additions & 0 deletions apps/app-frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@
"app.browse.server.installing": {
"message": "Installing"
},
"app.browse.server.world-fallback-name": {
"message": "Instance"
},
"app.content-install.no-compatible-versions": {
"message": "No available versions match {compatibilityLabel}. Select a version to install anyway. Dependencies will not be installed automatically."
},
Expand Down Expand Up @@ -326,6 +329,9 @@
"app.project.install-context.install-content-to-instance": {
"message": "Install content to instance"
},
"app.project.install-context.world-fallback-name": {
"message": "Instance"
},
"app.settings.developer-mode-enabled": {
"message": "Developer mode enabled."
},
Expand Down
Loading
Loading