mirror of
https://github.com/modrinth/code.git
synced 2026-09-04 22:10:15 +00:00
feat: qa + app routing bugs
This commit is contained in:
@@ -146,6 +146,7 @@ const APP_LEFT_NAV_WIDTH = '4rem'
|
|||||||
const APP_SIDEBAR_WIDTH = 300
|
const APP_SIDEBAR_WIDTH = 300
|
||||||
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
|
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
|
||||||
const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime()
|
const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime()
|
||||||
|
const ROUTE_SUSPENSE_TIMEOUT_MS = 60_000
|
||||||
const credentials = ref()
|
const credentials = ref()
|
||||||
const sidebarToggled = ref(true)
|
const sidebarToggled = ref(true)
|
||||||
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
|
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
|
||||||
@@ -155,6 +156,22 @@ const forceSidebar = computed(
|
|||||||
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
|
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
|
||||||
)
|
)
|
||||||
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
|
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 hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
|
||||||
const prideFundraiserEnabled = computed(
|
const prideFundraiserEnabled = computed(
|
||||||
() => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
|
() => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
|
||||||
@@ -1673,11 +1690,17 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
|||||||
>
|
>
|
||||||
{{ formatMessage(messages.authUnreachableBody) }}
|
{{ formatMessage(messages.authUnreachableBody) }}
|
||||||
</Admonition>
|
</Admonition>
|
||||||
<RouterView v-slot="{ Component }">
|
<RouterView v-slot="{ Component, route: viewRoute }">
|
||||||
<template v-if="Component">
|
<template v-if="Component">
|
||||||
<Suspense @pending="onSuspensePending" @resolve="onSuspenseResolve">
|
<KeepAlive :include="keepAliveRouteComponents" :max="3">
|
||||||
<component :is="Component"></component>
|
<Suspense
|
||||||
</Suspense>
|
:timeout="ROUTE_SUSPENSE_TIMEOUT_MS"
|
||||||
|
@pending="onSuspensePending"
|
||||||
|
@resolve="onSuspenseResolve"
|
||||||
|
>
|
||||||
|
<component :is="Component" :key="getRouteViewKey(viewRoute)"></component>
|
||||||
|
</Suspense>
|
||||||
|
</KeepAlive>
|
||||||
</template>
|
</template>
|
||||||
</RouterView>
|
</RouterView>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,13 +8,18 @@ import type { ComputedRef, Ref } from 'vue'
|
|||||||
import { onUnmounted, ref, shallowRef } from 'vue'
|
import { onUnmounted, ref, shallowRef } from 'vue'
|
||||||
import type { Router } from 'vue-router'
|
import type { Router } from 'vue-router'
|
||||||
|
|
||||||
|
import {
|
||||||
|
fetchCachedServerStatus,
|
||||||
|
getFreshCachedServerStatus,
|
||||||
|
} from '@/composables/instances/use-server-status-query'
|
||||||
import { process_listener } from '@/helpers/events'
|
import { process_listener } from '@/helpers/events'
|
||||||
import { kill, list as listInstances } from '@/helpers/instance'
|
import { kill, list as listInstances } from '@/helpers/instance'
|
||||||
import { get_by_instance_id } from '@/helpers/process'
|
import { get_by_instance_id } from '@/helpers/process'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
import { add_server_to_instance, getServerAddress, getServerLatency } from '@/helpers/worlds'
|
import { add_server_to_instance, getServerAddress } from '@/helpers/worlds'
|
||||||
|
|
||||||
interface BrowseServerInstance {
|
interface BrowseServerInstance {
|
||||||
|
id: string
|
||||||
name: string
|
name: string
|
||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
@@ -68,12 +73,10 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const debugLog = useDebugLogger('BrowseServer')
|
const debugLog = useDebugLogger('BrowseServer')
|
||||||
const serverPings = shallowRef<Record<string, number | undefined>>({})
|
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 runningServerProjects = ref<Record<string, string>>({})
|
||||||
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
|
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
|
||||||
const contextMenuRef = ref<ContextMenuHandle | null>(null)
|
const contextMenuRef = ref<ContextMenuHandle | null>(null)
|
||||||
let serverPingCacheActive = true
|
let serverPingsActive = true
|
||||||
let unlistenProcesses: (() => void) | null = null
|
let unlistenProcesses: (() => void) | null = null
|
||||||
|
|
||||||
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
|
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
|
||||||
@@ -146,37 +149,26 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
|||||||
})
|
})
|
||||||
const nextPings = { ...serverPings.value }
|
const nextPings = { ...serverPings.value }
|
||||||
for (const { hit, address } of pingsToFetch) {
|
for (const { hit, address } of pingsToFetch) {
|
||||||
if (serverPingCache.has(address)) {
|
const cachedStatus = getFreshCachedServerStatus(queryClient, address)
|
||||||
nextPings[hit.project_id] = serverPingCache.get(address)
|
if (cachedStatus) {
|
||||||
|
nextPings[hit.project_id] = cachedStatus.ping
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
serverPings.value = nextPings
|
serverPings.value = nextPings
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pingsToFetch.map(async ({ hit, address }) => {
|
pingsToFetch.map(async ({ hit, address }) => {
|
||||||
if (serverPingCache.has(address)) return
|
if (getFreshCachedServerStatus(queryClient, address)) return
|
||||||
|
|
||||||
let pending = pendingServerPings.get(address)
|
try {
|
||||||
if (!pending) {
|
const status = await fetchCachedServerStatus(queryClient, address)
|
||||||
pending = getServerLatency(address)
|
if (!serverPingsActive) return
|
||||||
.then((latency) => {
|
serverPings.value = { ...serverPings.value, [hit.project_id]: status.ping }
|
||||||
if (serverPingCacheActive) serverPingCache.set(address, latency)
|
} catch (error) {
|
||||||
return latency
|
console.error(`Failed to ping server ${address}:`, error)
|
||||||
})
|
if (!serverPingsActive) return
|
||||||
.catch((error) => {
|
serverPings.value = { ...serverPings.value, [hit.project_id]: undefined }
|
||||||
console.error(`Failed to ping server ${address}:`, error)
|
|
||||||
if (serverPingCacheActive) serverPingCache.set(address, undefined)
|
|
||||||
return undefined
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
pendingServerPings.delete(address)
|
|
||||||
})
|
|
||||||
pendingServerPings.set(address, pending)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const latency = await pending
|
|
||||||
if (!serverPingCacheActive) return
|
|
||||||
serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -308,10 +300,8 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
|||||||
.catch(options.handleError)
|
.catch(options.handleError)
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
serverPingCacheActive = false
|
serverPingsActive = false
|
||||||
unlistenProcesses?.()
|
unlistenProcesses?.()
|
||||||
serverPingCache.clear()
|
|
||||||
pendingServerPings.clear()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
requestInstall,
|
requestInstall,
|
||||||
SelectedProjectsFloatingBar,
|
SelectedProjectsFloatingBar,
|
||||||
useBrowseSearch,
|
useBrowseSearch,
|
||||||
useDebugLogger,
|
|
||||||
useStickyObserver,
|
useStickyObserver,
|
||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
@@ -61,19 +60,26 @@ import {
|
|||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
import { useTheming } from '@/store/state'
|
import { useTheming } from '@/store/state'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'Browse',
|
||||||
|
})
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const { installingServerProjects, playServerProject, showAddServerToInstanceModal } =
|
const { installingServerProjects, playServerProject, showAddServerToInstanceModal } =
|
||||||
injectServerInstall()
|
injectServerInstall()
|
||||||
const { install: installVersion } = injectContentInstall()
|
const { install: installVersion } = injectContentInstall()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const debugLog = useDebugLogger('Browse')
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
const browseRouteActive = computed(() => route.path.startsWith('/browse/'))
|
||||||
const serverSetupModalRef = ref<InstanceType<typeof CreationFlowModal> | null>(null)
|
const serverSetupModalRef = ref<InstanceType<typeof CreationFlowModal> | null>(null)
|
||||||
const serverInstallContent = createServerInstallContent({ serverSetupModalRef })
|
const serverInstallContent = createServerInstallContent({
|
||||||
|
serverSetupModalRef,
|
||||||
|
isRouteInContext: (targetRoute) => targetRoute.path.startsWith('/browse/'),
|
||||||
|
})
|
||||||
provideServerInstallContent(serverInstallContent)
|
provideServerInstallContent(serverInstallContent)
|
||||||
const {
|
const {
|
||||||
serverIdQuery,
|
serverIdQuery,
|
||||||
@@ -110,8 +116,6 @@ const {
|
|||||||
handleServerModpackFlowCreate,
|
handleServerModpackFlowCreate,
|
||||||
markServerProjectInstalled,
|
markServerProjectInstalled,
|
||||||
} = serverInstallContent
|
} = serverInstallContent
|
||||||
|
|
||||||
debugLog('fetching tags (categories, loaders, gameVersions)')
|
|
||||||
const [categories, loaders, availableGameVersions] = await Promise.all([
|
const [categories, loaders, availableGameVersions] = await Promise.all([
|
||||||
get_categories()
|
get_categories()
|
||||||
.catch(handleError)
|
.catch(handleError)
|
||||||
@@ -131,6 +135,7 @@ const tags: Ref<Tags> = computed(() => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
type Instance = {
|
type Instance = {
|
||||||
|
id: string
|
||||||
game_version: string
|
game_version: string
|
||||||
loader: string
|
loader: string
|
||||||
path: string
|
path: string
|
||||||
@@ -198,7 +203,6 @@ async function refreshInstalledProjectIds() {
|
|||||||
const serverProjectIds = worlds
|
const serverProjectIds = worlds
|
||||||
.filter((w) => w.type === 'server' && 'project_id' in w && w.project_id)
|
.filter((w) => w.type === 'server' && 'project_id' in w && w.project_id)
|
||||||
.map((w) => (w as { project_id: string }).project_id)
|
.map((w) => (w as { project_id: string }).project_id)
|
||||||
debugLog('installedServerProjectIds loaded', { count: serverProjectIds.length })
|
|
||||||
installedProjectIds.value = serverProjectIds
|
installedProjectIds.value = serverProjectIds
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -206,45 +210,29 @@ async function refreshInstalledProjectIds() {
|
|||||||
const ids = await getInstalledProjectIds(route.query.i as string).catch(handleError)
|
const ids = await getInstalledProjectIds(route.query.i as string).catch(handleError)
|
||||||
if (!ids) return
|
if (!ids) return
|
||||||
|
|
||||||
debugLog('installedProjectIds loaded', { count: ids.length })
|
|
||||||
installedProjectIds.value = ids
|
installedProjectIds.value = ids
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initInstanceContext() {
|
async function initInstanceContext() {
|
||||||
debugLog('initInstanceContext', {
|
|
||||||
queryI: route.query.i,
|
|
||||||
queryAi: route.query.ai,
|
|
||||||
querySid: route.query.sid,
|
|
||||||
queryWid: route.query.wid,
|
|
||||||
queryFrom: route.query.from,
|
|
||||||
})
|
|
||||||
await initServerContext()
|
await initServerContext()
|
||||||
|
|
||||||
if (route.query.i) {
|
if (route.query.i) {
|
||||||
instance.value = (await getInstance(route.query.i as string).catch(handleError)) ?? null
|
instance.value = (await getInstance(route.query.i as string).catch(handleError)) ?? null
|
||||||
debugLog('instance loaded', {
|
|
||||||
name: instance.value?.name,
|
|
||||||
loader: instance.value?.loader,
|
|
||||||
gameVersion: instance.value?.game_version,
|
|
||||||
})
|
|
||||||
|
|
||||||
await refreshInstalledProjectIds()
|
await refreshInstalledProjectIds()
|
||||||
|
|
||||||
if (instance.value?.link?.project_id) {
|
if (instance.value?.link?.project_id) {
|
||||||
debugLog('checking linked project for server status', instance.value.link.project_id)
|
|
||||||
const projectV3 = await get_project_v3(
|
const projectV3 = await get_project_v3(
|
||||||
instance.value.link.project_id,
|
instance.value.link.project_id,
|
||||||
'must_revalidate',
|
'must_revalidate',
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
if (projectV3?.minecraft_server != null) {
|
if (projectV3?.minecraft_server != null) {
|
||||||
debugLog('instance is a server instance')
|
|
||||||
isServerInstance.value = true
|
isServerInstance.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.query.ai && !(route.params.projectType === 'modpack')) {
|
if (route.query.ai && !(route.params.projectType === 'modpack')) {
|
||||||
debugLog('setting instanceHideInstalled from query', route.query.ai)
|
|
||||||
instanceHideInstalled.value = route.query.ai === 'true'
|
instanceHideInstalled.value = route.query.ai === 'true'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,7 +283,7 @@ function syncHiddenServerContentProjectIds() {
|
|||||||
watch(
|
watch(
|
||||||
serverContentProjectIds,
|
serverContentProjectIds,
|
||||||
() => {
|
() => {
|
||||||
if (!hiddenServerContentProjectIdsInitialized.value) {
|
if (!hiddenServerContentProjectIdsInitialized.value || serverHideInstalled.value) {
|
||||||
syncHiddenServerContentProjectIds()
|
syncHiddenServerContentProjectIds()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -368,11 +356,9 @@ const {
|
|||||||
|
|
||||||
const offline = ref(!navigator.onLine)
|
const offline = ref(!navigator.onLine)
|
||||||
window.addEventListener('offline', () => {
|
window.addEventListener('offline', () => {
|
||||||
debugLog('went offline')
|
|
||||||
offline.value = true
|
offline.value = true
|
||||||
})
|
})
|
||||||
window.addEventListener('online', () => {
|
window.addEventListener('online', () => {
|
||||||
debugLog('went online')
|
|
||||||
offline.value = false
|
offline.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -480,7 +466,6 @@ const projectType = ref<ProjectType>(route.params.projectType as ProjectType)
|
|||||||
function resetInstanceContext() {
|
function resetInstanceContext() {
|
||||||
if (!instance.value) return
|
if (!instance.value) return
|
||||||
|
|
||||||
debugLog('instance context removed, resetting')
|
|
||||||
instance.value = null
|
instance.value = null
|
||||||
installedProjectIds.value = null
|
installedProjectIds.value = null
|
||||||
instanceHideInstalled.value = false
|
instanceHideInstalled.value = false
|
||||||
@@ -495,6 +480,9 @@ function resetInstanceContext() {
|
|||||||
watch(
|
watch(
|
||||||
() => route.params.projectType as ProjectType,
|
() => route.params.projectType as ProjectType,
|
||||||
async (newType) => {
|
async (newType) => {
|
||||||
|
if (!browseRouteActive.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (isSetupServerContext.value) {
|
if (isSetupServerContext.value) {
|
||||||
enforceSetupModpackRoute(newType)
|
enforceSetupModpackRoute(newType)
|
||||||
if (newType !== 'modpack') return
|
if (newType !== 'modpack') return
|
||||||
@@ -502,7 +490,6 @@ watch(
|
|||||||
|
|
||||||
if (!newType || newType === projectType.value) return
|
if (!newType || newType === projectType.value) return
|
||||||
|
|
||||||
debugLog('projectType route param changed', { from: projectType.value, to: newType })
|
|
||||||
projectType.value = newType
|
projectType.value = newType
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -616,6 +603,7 @@ const installContext = computed(() => {
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
const stickyInstallHeaderRef = ref<HTMLElement | null>(null)
|
const stickyInstallHeaderRef = ref<HTMLElement | null>(null)
|
||||||
const { isStuck: isInstallHeaderStuck } = useStickyObserver(
|
const { isStuck: isInstallHeaderStuck } = useStickyObserver(
|
||||||
stickyInstallHeaderRef,
|
stickyInstallHeaderRef,
|
||||||
@@ -718,11 +706,10 @@ function getCardActions(
|
|||||||
installed?: boolean
|
installed?: boolean
|
||||||
installing?: boolean
|
installing?: boolean
|
||||||
}
|
}
|
||||||
const isInstalled =
|
const isInstalled = isServerContext.value
|
||||||
projectResult.installed ||
|
? serverContentProjectIds.value.has(projectResult.project_id || '') ||
|
||||||
allInstalledIds.value.has(projectResult.project_id || '') ||
|
serverContextServerData.value?.upstream?.project_id === projectResult.project_id
|
||||||
serverContentProjectIds.value.has(projectResult.project_id || '') ||
|
: projectResult.installed || allInstalledIds.value.has(projectResult.project_id || '')
|
||||||
serverContextServerData.value?.upstream?.project_id === projectResult.project_id
|
|
||||||
const isInstalling = installingProjectIds.value.has(projectResult.project_id)
|
const isInstalling = installingProjectIds.value.has(projectResult.project_id)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -886,7 +873,6 @@ function onSearchResultsInstalled(ids: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function search(requestParams: string) {
|
async function search(requestParams: string) {
|
||||||
debugLog('searching v3', requestParams)
|
|
||||||
const isServer = projectType.value === 'server'
|
const isServer = projectType.value === 'server'
|
||||||
|
|
||||||
const rawResults = await queryClient.fetchQuery({
|
const rawResults = await queryClient.fetchQuery({
|
||||||
@@ -968,6 +954,7 @@ const lockedFilterMessages = computed(() => ({
|
|||||||
const searchState = useBrowseSearch({
|
const searchState = useBrowseSearch({
|
||||||
projectType,
|
projectType,
|
||||||
tags,
|
tags,
|
||||||
|
active: browseRouteActive,
|
||||||
providedFilters: combinedProvidedFilters,
|
providedFilters: combinedProvidedFilters,
|
||||||
search,
|
search,
|
||||||
persistentQueryParams: ['i', 'ai', 'shi', 'sid', 'wid', 'from'],
|
persistentQueryParams: ['i', 'ai', 'shi', 'sid', 'wid', 'from'],
|
||||||
@@ -1043,7 +1030,12 @@ onUnmounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function getProjectBrowseQuery() {
|
function getProjectBrowseQuery() {
|
||||||
if (!installContext.value) return undefined
|
if (!browseRouteActive.value) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
if (!installContext.value) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...route.query,
|
...route.query,
|
||||||
b: route.fullPath,
|
b: route.fullPath,
|
||||||
@@ -1110,10 +1102,10 @@ provideBrowseManager({
|
|||||||
<div
|
<div
|
||||||
v-if="installContext"
|
v-if="installContext"
|
||||||
ref="stickyInstallHeaderRef"
|
ref="stickyInstallHeaderRef"
|
||||||
class="sticky top-0 z-20 -mx-6 -mt-6 rounded-tl-[--radius-xl] border-0 border-b border-solid bg-surface-1 p-3 border-surface-5"
|
class="sticky top-0 z-20 -mx-6 -mt-6 rounded-tl-[--radius-xl] border-0 border-b border-solid border-divider bg-surface-1 px-6 pt-6"
|
||||||
:class="[isInstallHeaderStuck ? 'border-t' : '']"
|
:class="[isInstallHeaderStuck ? 'border-t' : '']"
|
||||||
>
|
>
|
||||||
<BrowseInstallHeader />
|
<BrowseInstallHeader bottom-padding />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SelectedProjectsFloatingBar v-if="installContext" />
|
<SelectedProjectsFloatingBar v-if="installContext" />
|
||||||
@@ -1145,7 +1137,7 @@ provideBrowseManager({
|
|||||||
@create="handleServerModpackFlowCreate"
|
@create="handleServerModpackFlowCreate"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Teleport to="#sidebar-teleport-target">
|
<Teleport v-if="browseRouteActive" to="#sidebar-teleport-target">
|
||||||
<BrowseSidebar />
|
<BrowseSidebar />
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ import {
|
|||||||
ExternalIcon,
|
ExternalIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
|
TagCategoryGamepad2Icon as Gamepad2Icon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
HashIcon,
|
HashIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
@@ -135,7 +136,6 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
TagCategoryGamepad2Icon as Gamepad2Icon,
|
|
||||||
TerminalSquareIcon,
|
TerminalSquareIcon,
|
||||||
TimerIcon,
|
TimerIcon,
|
||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
@@ -145,9 +145,9 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
formatLoaderLabel,
|
formatLoaderLabel,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
LoaderIcon as ServerLoaderIcon,
|
|
||||||
NavTabs,
|
NavTabs,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
|
LoaderIcon as ServerLoaderIcon,
|
||||||
ServerOnlinePlayers,
|
ServerOnlinePlayers,
|
||||||
ServerPing,
|
ServerPing,
|
||||||
ServerRecentPlays,
|
ServerRecentPlays,
|
||||||
@@ -161,12 +161,16 @@ import dayjs from 'dayjs'
|
|||||||
import duration from 'dayjs/plugin/duration'
|
import duration from 'dayjs/plugin/duration'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
|
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { onBeforeRouteUpdate, useRoute, useRouter, type LocationQuery } from 'vue-router'
|
||||||
|
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||||
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
|
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
|
||||||
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
||||||
|
import {
|
||||||
|
fetchCachedServerStatus,
|
||||||
|
getFreshCachedServerStatus,
|
||||||
|
} from '@/composables/instances/use-server-status-query'
|
||||||
import { useInstanceConsole } from '@/composables/useInstanceConsole'
|
import { useInstanceConsole } from '@/composables/useInstanceConsole'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { get_project_v3 } from '@/helpers/cache.js'
|
import { get_project_v3 } from '@/helpers/cache.js'
|
||||||
@@ -177,7 +181,7 @@ import { type InstanceContentData, loadInstanceContentData } from '@/helpers/ins
|
|||||||
import { get_by_instance_id } from '@/helpers/process'
|
import { get_by_instance_id } from '@/helpers/process'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
import { createInstanceShortcut, showInstanceInFolder } from '@/helpers/utils.js'
|
import { createInstanceShortcut, showInstanceInFolder } from '@/helpers/utils.js'
|
||||||
import { get_server_status, refreshWorlds } from '@/helpers/worlds'
|
import { refreshWorlds, type ServerStatus } from '@/helpers/worlds'
|
||||||
import { injectServerInstall } from '@/providers/server-install'
|
import { injectServerInstall } from '@/providers/server-install'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import { useBreadcrumbs, useTheming } from '@/store/state'
|
import { useBreadcrumbs, useTheming } from '@/store/state'
|
||||||
@@ -222,13 +226,16 @@ const selected = ref<unknown[]>([])
|
|||||||
|
|
||||||
const minecraftServer = computed(() => linkedProjectV3.value?.minecraft_server)
|
const minecraftServer = computed(() => linkedProjectV3.value?.minecraft_server)
|
||||||
const javaServerPingData = computed(() => linkedProjectV3.value?.minecraft_java_server?.ping?.data)
|
const javaServerPingData = computed(() => linkedProjectV3.value?.minecraft_java_server?.ping?.data)
|
||||||
const statusOnline = computed(() => !!javaServerPingData.value)
|
const liveServerStatusOnline = ref(false)
|
||||||
|
const statusOnline = computed(() => liveServerStatusOnline.value || !!javaServerPingData.value)
|
||||||
const recentPlays = computed(
|
const recentPlays = computed(
|
||||||
() => linkedProjectV3.value?.minecraft_java_server?.verified_plays_2w ?? undefined,
|
() => linkedProjectV3.value?.minecraft_java_server?.verified_plays_2w ?? undefined,
|
||||||
)
|
)
|
||||||
const playersOnline = ref<number | undefined>(undefined)
|
const playersOnline = ref<number | undefined>(undefined)
|
||||||
const ping = ref<number | undefined>(undefined)
|
const ping = ref<number | undefined>(undefined)
|
||||||
const loadingServerPing = ref(false)
|
const loadingServerPing = ref(false)
|
||||||
|
const activeInstanceId = ref<string>()
|
||||||
|
let fetchInstanceRequestId = 0
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => router.currentRoute.value,
|
() => router.currentRoute.value,
|
||||||
@@ -240,24 +247,68 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
function isContentSubpageRoute(routeName = displayedInstanceRoute.value.name) {
|
type InstanceRouteContext = {
|
||||||
|
name: unknown
|
||||||
|
path: string
|
||||||
|
query: LocationQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstancePageData = {
|
||||||
|
instanceId: string
|
||||||
|
instance?: GameInstance
|
||||||
|
linkedProjectV3?: Labrinth.Projects.v3.Project
|
||||||
|
isServerInstance: boolean
|
||||||
|
preloadedContent: InstanceContentData | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyServerStatus(status: ServerStatus) {
|
||||||
|
playersOnline.value = status.players?.online
|
||||||
|
ping.value = status.ping
|
||||||
|
liveServerStatusOnline.value = true
|
||||||
|
loadingServerPing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function isContentSubpageRoute(routeName: unknown = displayedInstanceRoute.value.name) {
|
||||||
return typeof routeName === 'string' && contentSubpageRouteNames.has(routeName)
|
return typeof routeName === 'string' && contentSubpageRouteNames.has(routeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchInstance() {
|
function resetServerStatus() {
|
||||||
isServerInstance.value = false
|
|
||||||
linkedProjectV3.value = undefined
|
|
||||||
preloadedContent.value = null
|
|
||||||
ping.value = undefined
|
ping.value = undefined
|
||||||
playersOnline.value = undefined
|
playersOnline.value = undefined
|
||||||
|
liveServerStatusOnline.value = false
|
||||||
loadingServerPing.value = false
|
loadingServerPing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const nextInstance = await get(route.params.id as string).catch(handleError)
|
function isCurrentInstanceRequest(requestId: number, instanceId: string) {
|
||||||
|
return (
|
||||||
|
requestId === fetchInstanceRequestId &&
|
||||||
|
route.path.startsWith('/instance') &&
|
||||||
|
route.params.id === instanceId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInstanceBreadcrumbs(nextInstance: GameInstance, routeContext: InstanceRouteContext) {
|
||||||
|
breadcrumbs.setName(
|
||||||
|
'Instance',
|
||||||
|
nextInstance.name.length > 40 ? nextInstance.name.substring(0, 40) + '...' : nextInstance.name,
|
||||||
|
)
|
||||||
|
breadcrumbs.setContext({
|
||||||
|
name: nextInstance.name,
|
||||||
|
link: routeContext.path,
|
||||||
|
query: routeContext.query,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadInstancePageData(
|
||||||
|
instanceId: string,
|
||||||
|
routeName: unknown = displayedInstanceRoute.value.name,
|
||||||
|
): Promise<InstancePageData> {
|
||||||
|
const nextInstance = await get(instanceId).catch(handleError)
|
||||||
let nextLinkedProjectV3: Labrinth.Projects.v3.Project | undefined
|
let nextLinkedProjectV3: Labrinth.Projects.v3.Project | undefined
|
||||||
let nextIsServerInstance = false
|
let nextIsServerInstance = false
|
||||||
|
|
||||||
const contentPreloadPromise =
|
const contentPreloadPromise =
|
||||||
nextInstance && isContentSubpageRoute()
|
nextInstance && isContentSubpageRoute(routeName)
|
||||||
? loadInstanceContentData(nextInstance.id, undefined, handleError)
|
? loadInstanceContentData(nextInstance.id, undefined, handleError)
|
||||||
: Promise.resolve(null)
|
: Promise.resolve(null)
|
||||||
|
|
||||||
@@ -275,59 +326,113 @@ async function fetchInstance() {
|
|||||||
|
|
||||||
const nextPreloadedContent = await contentPreloadPromise
|
const nextPreloadedContent = await contentPreloadPromise
|
||||||
|
|
||||||
instance.value = nextInstance ?? undefined
|
return {
|
||||||
linkedProjectV3.value = nextLinkedProjectV3
|
instanceId,
|
||||||
isServerInstance.value = nextIsServerInstance
|
instance: nextInstance ?? undefined,
|
||||||
preloadedContent.value = nextPreloadedContent
|
linkedProjectV3: nextLinkedProjectV3,
|
||||||
|
isServerInstance: nextIsServerInstance,
|
||||||
|
preloadedContent: nextPreloadedContent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fetchDeferredData()
|
function applyInstancePageData(data: InstancePageData, routeContext: InstanceRouteContext) {
|
||||||
|
activeInstanceId.value = data.instanceId
|
||||||
|
resetServerStatus()
|
||||||
|
playing.value = false
|
||||||
|
|
||||||
if (nextInstance) {
|
instance.value = data.instance
|
||||||
|
linkedProjectV3.value = data.linkedProjectV3
|
||||||
|
isServerInstance.value = data.isServerInstance
|
||||||
|
preloadedContent.value = data.preloadedContent
|
||||||
|
|
||||||
|
if (data.instance) {
|
||||||
|
setInstanceBreadcrumbs(data.instance, routeContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchDeferredData(data.instanceId)
|
||||||
|
|
||||||
|
if (data.instance) {
|
||||||
queryClient.prefetchQuery({
|
queryClient.prefetchQuery({
|
||||||
queryKey: ['worlds', nextInstance.id],
|
queryKey: ['worlds', data.instance.id],
|
||||||
queryFn: () => refreshWorlds(nextInstance.id),
|
queryFn: () => refreshWorlds(data.instance!.id),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchDeferredData() {
|
async function fetchInstance(instanceId = route.params.id as string) {
|
||||||
|
const requestId = ++fetchInstanceRequestId
|
||||||
|
const data = await loadInstancePageData(instanceId)
|
||||||
|
if (!isCurrentInstanceRequest(requestId, instanceId)) return
|
||||||
|
|
||||||
|
applyInstancePageData(data, {
|
||||||
|
name: route.name,
|
||||||
|
path: route.path,
|
||||||
|
query: route.query,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchDeferredData(instanceId: string) {
|
||||||
const serverAddress = linkedProjectV3.value?.minecraft_java_server?.address
|
const serverAddress = linkedProjectV3.value?.minecraft_java_server?.address
|
||||||
if (isServerInstance.value && serverAddress) {
|
if (isServerInstance.value && serverAddress) {
|
||||||
get_server_status(serverAddress)
|
const cachedStatus = getFreshCachedServerStatus(queryClient, serverAddress)
|
||||||
|
if (cachedStatus) {
|
||||||
|
applyServerStatus(cachedStatus)
|
||||||
|
} else {
|
||||||
|
playersOnline.value = undefined
|
||||||
|
ping.value = undefined
|
||||||
|
loadingServerPing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCachedServerStatus(queryClient, serverAddress)
|
||||||
.then((status) => {
|
.then((status) => {
|
||||||
playersOnline.value = status.players?.online
|
if (
|
||||||
ping.value = status.ping
|
activeInstanceId.value !== instanceId ||
|
||||||
|
linkedProjectV3.value?.minecraft_java_server?.address !== serverAddress
|
||||||
|
)
|
||||||
|
return
|
||||||
|
applyServerStatus(status)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(`Failed to fetch server status for ${serverAddress}:`, error)
|
console.error(`Failed to fetch server status for ${serverAddress}:`, error)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
if (activeInstanceId.value !== instanceId) return
|
||||||
loadingServerPing.value = true
|
loadingServerPing.value = true
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
loadingServerPing.value = true
|
loadingServerPing.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePlayState()
|
updatePlayState(instanceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updatePlayState() {
|
async function updatePlayState(instanceId = route.params.id as string) {
|
||||||
if (!route.params.id) return
|
if (!instanceId) return
|
||||||
const runningProcesses = await get_by_instance_id(route.params.id as string).catch(handleError)
|
const runningProcesses = await get_by_instance_id(instanceId).catch(handleError)
|
||||||
|
if (activeInstanceId.value !== instanceId) return
|
||||||
|
|
||||||
playing.value = Array.isArray(runningProcesses) && runningProcesses.length > 0
|
playing.value = Array.isArray(runningProcesses) && runningProcesses.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchInstance()
|
await fetchInstance(route.params.id as string)
|
||||||
watch(
|
|
||||||
() => route.params.id,
|
onBeforeRouteUpdate(async (to) => {
|
||||||
async () => {
|
if (!to.path.startsWith('/instance')) return
|
||||||
if (route.params.id && route.path.startsWith('/instance')) {
|
const instanceId = Array.isArray(to.params.id) ? to.params.id[0] : to.params.id
|
||||||
await fetchInstance()
|
if (typeof instanceId !== 'string') return false
|
||||||
}
|
|
||||||
},
|
const requestId = ++fetchInstanceRequestId
|
||||||
)
|
const data = await loadInstancePageData(instanceId, to.name)
|
||||||
|
if (requestId !== fetchInstanceRequestId) return false
|
||||||
|
|
||||||
|
displayedInstanceRoute.value = to
|
||||||
|
applyInstancePageData(data, {
|
||||||
|
name: to.name,
|
||||||
|
path: to.path,
|
||||||
|
query: to.query,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const basePath = computed(
|
const basePath = computed(
|
||||||
() => `/instance/${encodeURIComponent(displayedInstanceRoute.value.params.id as string)}`,
|
() => `/instance/${encodeURIComponent(displayedInstanceRoute.value.params.id as string)}`,
|
||||||
@@ -372,20 +477,6 @@ const tabs = computed(() => [
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
if (instance.value) {
|
|
||||||
breadcrumbs.setName(
|
|
||||||
'Instance',
|
|
||||||
instance.value.name.length > 40
|
|
||||||
? instance.value.name.substring(0, 40) + '...'
|
|
||||||
: instance.value.name,
|
|
||||||
)
|
|
||||||
breadcrumbs.setContext({
|
|
||||||
name: instance.value.name,
|
|
||||||
link: displayedInstanceRoute.value.path,
|
|
||||||
query: displayedInstanceRoute.value.query,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = ref<InstanceType<typeof ContextMenu> | null>(null)
|
const options = ref<InstanceType<typeof ContextMenu> | null>(null)
|
||||||
|
|
||||||
const startInstance = async (context: string) => {
|
const startInstance = async (context: string) => {
|
||||||
|
|||||||
@@ -47,9 +47,9 @@
|
|||||||
<div class="flex flex-col gap-4 p-6">
|
<div class="flex flex-col gap-4 p-6">
|
||||||
<div
|
<div
|
||||||
v-if="projectInstallContext"
|
v-if="projectInstallContext"
|
||||||
class="sticky top-0 z-20 -mx-6 -mt-6 rounded-tl-[--radius-xl] border-0 border-b border-solid bg-surface-1 p-3 border-surface-5"
|
class="sticky top-0 z-20 -mx-6 -mt-6 rounded-tl-[--radius-xl] border-0 border-b border-solid border-divider bg-surface-1 px-6 pt-6"
|
||||||
>
|
>
|
||||||
<BrowseInstallHeader :install-context="projectInstallContext" />
|
<BrowseInstallHeader :install-context="projectInstallContext" bottom-padding />
|
||||||
</div>
|
</div>
|
||||||
<InstanceIndicator v-if="instance && !projectInstallContext" :instance="instance" />
|
<InstanceIndicator v-if="instance && !projectInstallContext" :instance="instance" />
|
||||||
<template v-if="data">
|
<template v-if="data">
|
||||||
@@ -172,6 +172,7 @@ import {
|
|||||||
SelectedProjectsFloatingBar,
|
SelectedProjectsFloatingBar,
|
||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
@@ -181,6 +182,10 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
|
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||||
|
import {
|
||||||
|
fetchCachedServerStatus,
|
||||||
|
getFreshCachedServerStatus,
|
||||||
|
} from '@/composables/instances/use-server-status-query'
|
||||||
import {
|
import {
|
||||||
get_organization,
|
get_organization,
|
||||||
get_project,
|
get_project,
|
||||||
@@ -199,7 +204,7 @@ import {
|
|||||||
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata'
|
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata'
|
||||||
import { get_by_instance_id } from '@/helpers/process'
|
import { get_by_instance_id } from '@/helpers/process'
|
||||||
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||||
import { getServerAddress, getServerLatency } from '@/helpers/worlds'
|
import { getServerAddress } from '@/helpers/worlds'
|
||||||
import { injectContentInstall } from '@/providers/content-install'
|
import { injectContentInstall } from '@/providers/content-install'
|
||||||
import { injectServerInstall } from '@/providers/server-install'
|
import { injectServerInstall } from '@/providers/server-install'
|
||||||
import { createServerInstallContent } from '@/providers/setup/server-install-content'
|
import { createServerInstallContent } from '@/providers/setup/server-install-content'
|
||||||
@@ -212,6 +217,7 @@ const { handleError } = injectNotificationManager()
|
|||||||
const { install: installVersion } = injectContentInstall()
|
const { install: installVersion } = injectContentInstall()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const breadcrumbs = useBreadcrumbs()
|
const breadcrumbs = useBreadcrumbs()
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
@@ -225,6 +231,10 @@ const messages = defineMessages({
|
|||||||
id: 'app.project.install-context.install-content-to-instance',
|
id: 'app.project.install-context.install-content-to-instance',
|
||||||
defaultMessage: 'Install content to instance',
|
defaultMessage: 'Install content to instance',
|
||||||
},
|
},
|
||||||
|
worldFallbackName: {
|
||||||
|
id: 'app.project.install-context.world-fallback-name',
|
||||||
|
defaultMessage: 'Instance',
|
||||||
|
},
|
||||||
alreadyInstalled: {
|
alreadyInstalled: {
|
||||||
id: 'app.project.install-button.already-installed',
|
id: 'app.project.install-button.already-installed',
|
||||||
defaultMessage: 'This project is already installed',
|
defaultMessage: 'This project is already installed',
|
||||||
@@ -255,7 +265,10 @@ const serverStatusOnline = ref(false)
|
|||||||
const serverInstancePath = ref(null)
|
const serverInstancePath = ref(null)
|
||||||
const serverPlaying = ref(false)
|
const serverPlaying = ref(false)
|
||||||
const serverSetupModalRef = ref(null)
|
const serverSetupModalRef = ref(null)
|
||||||
const serverInstallContent = createServerInstallContent({ serverSetupModalRef })
|
const serverInstallContent = createServerInstallContent({
|
||||||
|
serverSetupModalRef,
|
||||||
|
isRouteInContext: (targetRoute) => targetRoute.path.startsWith('/project/'),
|
||||||
|
})
|
||||||
|
|
||||||
serverInstallContent.watchServerContextChanges()
|
serverInstallContent.watchServerContextChanges()
|
||||||
await serverInstallContent.initServerContext()
|
await serverInstallContent.initServerContext()
|
||||||
@@ -322,7 +335,9 @@ const projectInstallContext = computed(() => {
|
|||||||
const serverData = serverInstallContent.serverContextServerData.value
|
const serverData = serverInstallContent.serverContextServerData.value
|
||||||
if (serverData) {
|
if (serverData) {
|
||||||
return {
|
return {
|
||||||
name: serverData.name,
|
name:
|
||||||
|
serverInstallContent.serverContextWorldName.value ??
|
||||||
|
formatMessage(messages.worldFallbackName),
|
||||||
loader: serverData.loader ?? '',
|
loader: serverData.loader ?? '',
|
||||||
gameVersion: serverData.mc_version ?? '',
|
gameVersion: serverData.mc_version ?? '',
|
||||||
serverId: serverInstallContent.serverIdQuery.value,
|
serverId: serverInstallContent.serverIdQuery.value,
|
||||||
@@ -611,10 +626,19 @@ async function fetchProjectData() {
|
|||||||
function fetchDeferredServerData(project) {
|
function fetchDeferredServerData(project) {
|
||||||
const serverAddress = projectV3.value?.minecraft_java_server?.address
|
const serverAddress = projectV3.value?.minecraft_java_server?.address
|
||||||
if (serverAddress) {
|
if (serverAddress) {
|
||||||
serverPing.value = undefined
|
const cachedStatus = getFreshCachedServerStatus(queryClient, serverAddress)
|
||||||
getServerLatency(serverAddress)
|
if (cachedStatus) {
|
||||||
.then((latency) => {
|
serverPing.value = cachedStatus.ping
|
||||||
serverPing.value = latency
|
serverStatusOnline.value = true
|
||||||
|
} else {
|
||||||
|
serverPing.value = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCachedServerStatus(queryClient, serverAddress)
|
||||||
|
.then((status) => {
|
||||||
|
if (projectV3.value?.minecraft_java_server?.address !== serverAddress) return
|
||||||
|
serverPing.value = status.ping
|
||||||
|
serverStatusOnline.value = true
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(`Failed to ping server ${serverAddress}:`, error)
|
console.error(`Failed to ping server ${serverAddress}:`, error)
|
||||||
|
|||||||
@@ -18,7 +18,18 @@ import {
|
|||||||
writeStoredServerInstallQueue,
|
writeStoredServerInstallQueue,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import { computed, type ComputedRef, nextTick, type Ref, ref, watch } from 'vue'
|
import {
|
||||||
|
computed,
|
||||||
|
type ComputedRef,
|
||||||
|
nextTick,
|
||||||
|
onActivated,
|
||||||
|
onDeactivated,
|
||||||
|
type Ref,
|
||||||
|
ref,
|
||||||
|
shallowRef,
|
||||||
|
watch,
|
||||||
|
} from 'vue'
|
||||||
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
type ServerFlowFrom = 'onboarding' | 'reset-server'
|
type ServerFlowFrom = 'onboarding' | 'reset-server'
|
||||||
@@ -204,6 +215,7 @@ async function getQueuedInstallPlaceholders(
|
|||||||
|
|
||||||
export function createServerInstallContent(opts: {
|
export function createServerInstallContent(opts: {
|
||||||
serverSetupModalRef: Ref<ServerSetupModalHandle | null>
|
serverSetupModalRef: Ref<ServerSetupModalHandle | null>
|
||||||
|
isRouteInContext?: (route: RouteLocationNormalizedLoaded) => boolean
|
||||||
}) {
|
}) {
|
||||||
const { serverSetupModalRef } = opts
|
const { serverSetupModalRef } = opts
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -212,9 +224,22 @@ export function createServerInstallContent(opts: {
|
|||||||
const { handleError } = injectNotificationManager()
|
const { handleError } = injectNotificationManager()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const serverIdQuery = computed(() => readQueryString(route.query.sid))
|
const routeInContext = computed(() => opts.isRouteInContext?.(route) ?? true)
|
||||||
const worldIdQuery = computed(() => readQueryString(route.query.wid))
|
const contextQuery = shallowRef(route.query)
|
||||||
const browseFrom = computed(() => readQueryString(route.query.from))
|
|
||||||
|
watch(
|
||||||
|
[() => route.fullPath, routeInContext],
|
||||||
|
() => {
|
||||||
|
if (routeInContext.value) {
|
||||||
|
contextQuery.value = route.query
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const serverIdQuery = computed(() => readQueryString(contextQuery.value.sid))
|
||||||
|
const worldIdQuery = computed(() => readQueryString(contextQuery.value.wid))
|
||||||
|
const browseFrom = computed(() => readQueryString(contextQuery.value.from))
|
||||||
const serverFlowFrom = computed<ServerFlowFrom | null>(() =>
|
const serverFlowFrom = computed<ServerFlowFrom | null>(() =>
|
||||||
browseFrom.value === 'onboarding' || browseFrom.value === 'reset-server'
|
browseFrom.value === 'onboarding' || browseFrom.value === 'reset-server'
|
||||||
? browseFrom.value
|
? browseFrom.value
|
||||||
@@ -233,6 +258,7 @@ export function createServerInstallContent(opts: {
|
|||||||
const queuedServerInstalls = ref<Map<string, BrowseInstallPlan<InstallableSearchResult>>>(
|
const queuedServerInstalls = ref<Map<string, BrowseInstallPlan<InstallableSearchResult>>>(
|
||||||
new Map(),
|
new Map(),
|
||||||
)
|
)
|
||||||
|
const componentActive = ref(true)
|
||||||
const queuedServerInstallProjectIds = computed(() => new Set(queuedServerInstalls.value.keys()))
|
const queuedServerInstallProjectIds = computed(() => new Set(queuedServerInstalls.value.keys()))
|
||||||
const queuedServerInstallCount = computed(() => queuedServerInstalls.value.size)
|
const queuedServerInstallCount = computed(() => queuedServerInstalls.value.size)
|
||||||
const selectedServerInstallProjects = computed<BrowseSelectedProject[]>(() =>
|
const selectedServerInstallProjects = computed<BrowseSelectedProject[]>(() =>
|
||||||
@@ -280,6 +306,14 @@ export function createServerInstallContent(opts: {
|
|||||||
return 'Installing content'
|
return 'Installing content'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
componentActive.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
componentActive.value = false
|
||||||
|
})
|
||||||
|
|
||||||
async function getServerContextServerFull(serverId: string) {
|
async function getServerContextServerFull(serverId: string) {
|
||||||
if (serverContextServerFull.value?.id === serverId) {
|
if (serverContextServerFull.value?.id === serverId) {
|
||||||
return serverContextServerFull.value
|
return serverContextServerFull.value
|
||||||
@@ -349,53 +383,79 @@ export function createServerInstallContent(opts: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function watchServerContextChanges() {
|
function watchServerContextChanges() {
|
||||||
watch([serverIdQuery, effectiveServerWorldId], async ([sid, wid], [prevSid, prevWid]) => {
|
watch(
|
||||||
if (!sid) {
|
[componentActive, routeInContext, serverIdQuery, effectiveServerWorldId],
|
||||||
serverContextServerData.value = null
|
async ([active, inContext, sid, wid], [prevActive, prevInContext, prevSid, prevWid]) => {
|
||||||
serverContextServerFull.value = null
|
if (!active || !inContext) return
|
||||||
serverContentProjectIds.value = new Set()
|
|
||||||
serverContentInstallKeys.value = new Set()
|
|
||||||
setQueuedServerInstallPlans(new Map())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sid !== prevSid) {
|
if (!sid) {
|
||||||
serverContextWorldId.value = worldIdQuery.value
|
serverContextServerData.value = null
|
||||||
serverContextServerFull.value = null
|
serverContextServerFull.value = null
|
||||||
serverContentProjectIds.value = new Set()
|
serverContentProjectIds.value = new Set()
|
||||||
serverContentInstallKeys.value = new Set()
|
serverContentInstallKeys.value = new Set()
|
||||||
queuedServerInstalls.value = readStoredServerInstallQueue(sid, wid)
|
setQueuedServerInstallPlans(new Map())
|
||||||
try {
|
return
|
||||||
serverContextServerData.value = await client.archon.servers_v0.get(sid)
|
|
||||||
} catch (err) {
|
|
||||||
handleError(err as Error)
|
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const serverFull = await getServerContextServerFull(sid)
|
const hasServerDataForRoute = serverContextServerData.value?.server_id === sid
|
||||||
if (!worldIdQuery.value) {
|
const hasServerFullForRoute = serverContextServerFull.value?.id === sid
|
||||||
const activeWorld = serverFull.worlds.find((world) => world.is_active)
|
const didEnterContext = !prevActive || !prevInContext
|
||||||
serverContextWorldId.value = activeWorld?.id ?? serverFull.worlds[0]?.id ?? null
|
const shouldReloadRouteContext =
|
||||||
|
didEnterContext ||
|
||||||
|
sid !== prevSid ||
|
||||||
|
wid !== prevWid ||
|
||||||
|
!hasServerDataForRoute ||
|
||||||
|
!hasServerFullForRoute
|
||||||
|
|
||||||
|
if (!hasServerDataForRoute || !hasServerFullForRoute) {
|
||||||
|
serverContextWorldId.value = worldIdQuery.value
|
||||||
|
if (!hasServerDataForRoute) {
|
||||||
|
serverContextServerData.value = null
|
||||||
}
|
}
|
||||||
} catch (err) {
|
if (!hasServerFullForRoute) {
|
||||||
handleError(err as Error)
|
serverContextServerFull.value = null
|
||||||
|
}
|
||||||
|
serverContentProjectIds.value = new Set()
|
||||||
|
serverContentInstallKeys.value = new Set()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (wid !== prevWid) {
|
if (!hasServerDataForRoute) {
|
||||||
queuedServerInstalls.value = readStoredServerInstallQueue(sid, wid)
|
try {
|
||||||
}
|
serverContextServerData.value = await client.archon.servers_v0.get(sid)
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (wid && (sid !== prevSid || wid !== prevWid)) {
|
if (!hasServerFullForRoute) {
|
||||||
await refreshServerInstalledContent(sid, wid)
|
try {
|
||||||
}
|
const serverFull = await getServerContextServerFull(sid)
|
||||||
})
|
if (!worldIdQuery.value) {
|
||||||
|
const activeWorld = serverFull.worlds.find((world) => world.is_active)
|
||||||
|
serverContextWorldId.value = activeWorld?.id ?? serverFull.worlds[0]?.id ?? null
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldReloadRouteContext) {
|
||||||
|
queuedServerInstalls.value = readStoredServerInstallQueue(sid, wid)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wid && shouldReloadRouteContext) {
|
||||||
|
await refreshServerInstalledContent(sid, wid)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function enforceSetupModpackRoute(currentProjectType: string | undefined) {
|
function enforceSetupModpackRoute(currentProjectType: string | undefined) {
|
||||||
if (!isSetupServerContext.value || currentProjectType === 'modpack') return
|
if (!routeInContext.value || !isSetupServerContext.value || currentProjectType === 'modpack')
|
||||||
|
return
|
||||||
router.replace({
|
router.replace({
|
||||||
path: '/browse/modpack',
|
path: '/browse/modpack',
|
||||||
query: route.query,
|
query: contextQuery.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import * as Instance from '@/pages/instance'
|
|||||||
import * as Library from '@/pages/library'
|
import * as Library from '@/pages/library'
|
||||||
import * as Project from '@/pages/project'
|
import * as Project from '@/pages/project'
|
||||||
|
|
||||||
|
function getQueryParam(value) {
|
||||||
|
if (Array.isArray(value)) return value[0] ?? ''
|
||||||
|
return value ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures application routing. Add page to pages/index and then add to route table here.
|
* Configures application routing. Add page to pages/index and then add to route table here.
|
||||||
*/
|
*/
|
||||||
@@ -131,6 +136,17 @@ export default new createRouter({
|
|||||||
component: Pages.Browse,
|
component: Pages.Browse,
|
||||||
meta: {
|
meta: {
|
||||||
useContext: true,
|
useContext: true,
|
||||||
|
keepAliveComponent: 'Browse',
|
||||||
|
keepAliveKey: (route) => {
|
||||||
|
return [
|
||||||
|
'browse',
|
||||||
|
getQueryParam(route.params.projectType),
|
||||||
|
getQueryParam(route.query.i),
|
||||||
|
getQueryParam(route.query.sid),
|
||||||
|
getQueryParam(route.query.wid),
|
||||||
|
getQueryParam(route.query.from),
|
||||||
|
].join(':')
|
||||||
|
},
|
||||||
breadcrumb: [{ name: '?BrowseTitle' }],
|
breadcrumb: [{ name: '?BrowseTitle' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
class="flex flex-1 flex-col gap-3 border-0 border-y bg-surface-2 border-solid border-surface-5 my-auto px-5 py-4"
|
class="flex flex-1 flex-col gap-3 border-0 border-y bg-surface-2 border-solid border-surface-5 my-auto px-5 py-4"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="grid min-h-6 grid-cols-[minmax(0,1fr)_minmax(0,70%)] items-center gap-4 text-secondary [&>*:last-child]:max-w-full [&>*:last-child]:justify-self-end"
|
class="grid min-h-6 grid-cols-[minmax(0,1fr)_minmax(0,70%)] items-center gap-4 text-base text-secondary [&>*:last-child]:max-w-full [&>*:last-child]:justify-self-end"
|
||||||
>
|
>
|
||||||
<span>{{ formatMessage(commonMessages.modpackLabel) }}</span>
|
<span>{{ formatMessage(commonMessages.modpackLabel) }}</span>
|
||||||
<div
|
<div
|
||||||
@@ -146,22 +146,22 @@
|
|||||||
</span>
|
</span>
|
||||||
</AutoLink>
|
</AutoLink>
|
||||||
</div>
|
</div>
|
||||||
<span v-else class="font-semibold text-contrast">{{ formatMessage(messages.none) }}</span>
|
<span v-else class="font-semibold text-contrast">{{ formatMessage(messages.noModpack) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="grid min-h-6 grid-cols-[minmax(0,1fr)_minmax(0,25%)] items-center gap-4 text-secondary [&>*:last-child]:max-w-full [&>*:last-child]:justify-self-end"
|
class="grid min-h-6 grid-cols-[minmax(0,1fr)_minmax(0,25%)] items-center gap-4 text-base text-secondary [&>*:last-child]:max-w-full [&>*:last-child]:justify-self-end"
|
||||||
>
|
>
|
||||||
<span>{{ formatMessage(messages.installedContent) }}</span>
|
<span>{{ formatMessage(messages.installedContent) }}</span>
|
||||||
<span class="font-semibold text-contrast">{{ installedContentLabel }}</span>
|
<span class="font-semibold text-contrast">{{ installedContentLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="grid min-h-6 grid-cols-[minmax(0,1fr)_minmax(0,45%)] items-center gap-4 text-base font-medium text-secondary [&>*:last-child]:max-w-full [&>*:last-child]:justify-self-end"
|
class="grid min-h-6 grid-cols-[minmax(0,1fr)_minmax(0,45%)] items-center gap-4 text-base text-secondary [&>*:last-child]:max-w-full [&>*:last-child]:justify-self-end"
|
||||||
>
|
>
|
||||||
<span>{{ formatMessage(messages.lastActive) }}</span>
|
<span>{{ formatMessage(messages.lastActive) }}</span>
|
||||||
<span class="font-semibold text-contrast">{{ lastActiveLabel }}</span>
|
<span class="font-semibold text-contrast">{{ lastActiveLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="grid min-h-6 grid-cols-[minmax(0,1fr)_minmax(0,45%)] items-center gap-4 text-base font-medium text-secondary [&>*:last-child]:max-w-full [&>*:last-child]:justify-self-end"
|
class="grid min-h-6 grid-cols-[minmax(0,1fr)_minmax(0,45%)] items-center gap-4 text-base text-secondary [&>*:last-child]:max-w-full [&>*:last-child]:justify-self-end"
|
||||||
>
|
>
|
||||||
<span>{{ formatMessage(messages.created) }}</span>
|
<span>{{ formatMessage(messages.created) }}</span>
|
||||||
<span class="font-semibold text-contrast">{{ createdLabel }}</span>
|
<span class="font-semibold text-contrast">{{ createdLabel }}</span>
|
||||||
@@ -191,6 +191,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PencilIcon, PlusIcon, Settings2Icon } from '@modrinth/assets'
|
import { PencilIcon, PlusIcon, Settings2Icon } from '@modrinth/assets'
|
||||||
|
import { capitalizeString } from '@modrinth/utils'
|
||||||
import { computed, useId, useTemplateRef } from 'vue'
|
import { computed, useId, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
import AutoLink from '#ui/components/base/AutoLink.vue'
|
import AutoLink from '#ui/components/base/AutoLink.vue'
|
||||||
@@ -215,9 +216,9 @@ const messages = defineMessages({
|
|||||||
id: 'servers.manage.instances.card.active',
|
id: 'servers.manage.instances.card.active',
|
||||||
defaultMessage: 'Active',
|
defaultMessage: 'Active',
|
||||||
},
|
},
|
||||||
none: {
|
noModpack: {
|
||||||
id: 'servers.manage.instances.card.none',
|
id: 'servers.manage.instances.card.no-modpack',
|
||||||
defaultMessage: 'None',
|
defaultMessage: '—',
|
||||||
},
|
},
|
||||||
installedContent: {
|
installedContent: {
|
||||||
id: 'servers.manage.instances.card.installed-content',
|
id: 'servers.manage.instances.card.installed-content',
|
||||||
@@ -298,7 +299,7 @@ const installedContentLabel = computed(() => {
|
|||||||
const lastActiveLabel = computed(() => {
|
const lastActiveLabel = computed(() => {
|
||||||
if (props.world.type === 'empty') return ''
|
if (props.world.type === 'empty') return ''
|
||||||
return props.world.lastActiveAt
|
return props.world.lastActiveAt
|
||||||
? formatRelativeTime(props.world.lastActiveAt)
|
? capitalizeString(formatRelativeTime(props.world.lastActiveAt))
|
||||||
: formatMessage(messages.notTrackedYet)
|
: formatMessage(messages.notTrackedYet)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { ComputedRef, Ref, ShallowRef } from 'vue'
|
|||||||
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
|
||||||
import type { FilterType, FilterValue, ProjectType, SortType } from '#ui/utils/search'
|
import type { FilterType, FilterValue, ProjectType, SortType } from '#ui/utils/search'
|
||||||
import { LOADER_FILTER_TYPES, useSearch } from '#ui/utils/search'
|
import { LOADER_FILTER_TYPES, useSearch } from '#ui/utils/search'
|
||||||
import { useServerSearch } from '#ui/utils/server-search'
|
import { useServerSearch } from '#ui/utils/server-search'
|
||||||
@@ -18,6 +17,7 @@ export interface UseBrowseSearchOptions {
|
|||||||
categories: Labrinth.Tags.v2.Category[]
|
categories: Labrinth.Tags.v2.Category[]
|
||||||
}>
|
}>
|
||||||
providedFilters?: ComputedRef<FilterValue[]>
|
providedFilters?: ComputedRef<FilterValue[]>
|
||||||
|
active?: ComputedRef<boolean>
|
||||||
search: (params: string) => Promise<BrowseSearchResponse>
|
search: (params: string) => Promise<BrowseSearchResponse>
|
||||||
persistentQueryParams: string[]
|
persistentQueryParams: string[]
|
||||||
getExtraQueryParams?: () => Record<string, string | undefined>
|
getExtraQueryParams?: () => Record<string, string | undefined>
|
||||||
@@ -61,12 +61,10 @@ export interface BrowseSearchState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchState {
|
export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchState {
|
||||||
const debug = useDebugLogger('BrowseSearch')
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
debug('init, projectType:', options.projectType.value)
|
const active = computed(() => options.active?.value ?? true)
|
||||||
|
|
||||||
const projectTypes = computed(() => [options.projectType.value] as ProjectType[])
|
const projectTypes = computed(() => [options.projectType.value] as ProjectType[])
|
||||||
const isServerType = computed(() => options.projectType.value === 'server')
|
const isServerType = computed(() => options.projectType.value === 'server')
|
||||||
|
|
||||||
@@ -172,6 +170,13 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
|
|||||||
let searchVersion = 0
|
let searchVersion = 0
|
||||||
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
function clearSearchDebounce() {
|
||||||
|
if (searchDebounceTimer) {
|
||||||
|
clearTimeout(searchDebounceTimer)
|
||||||
|
searchDebounceTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const providedFiltersOrEmpty = computed(() => options.providedFilters?.value ?? [])
|
const providedFiltersOrEmpty = computed(() => options.providedFilters?.value ?? [])
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -192,24 +197,29 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
|
|||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(effectiveRequestParams, (newVal, oldVal) => {
|
watch(effectiveRequestParams, () => {
|
||||||
debug('effectiveRequestParams changed', {
|
clearSearchDebounce()
|
||||||
from: oldVal?.substring(0, 80),
|
if (!active.value) {
|
||||||
to: newVal?.substring(0, 80),
|
return
|
||||||
})
|
}
|
||||||
if (searchDebounceTimer) clearTimeout(searchDebounceTimer)
|
|
||||||
searchDebounceTimer = setTimeout(() => {
|
searchDebounceTimer = setTimeout(() => {
|
||||||
refreshSearch()
|
refreshSearch()
|
||||||
}, 200)
|
}, 200)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(active, (isActive, wasActive) => {
|
||||||
|
clearSearchDebounce()
|
||||||
|
if (isActive && wasActive === false) {
|
||||||
|
void refreshSearch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function refreshSearch() {
|
async function refreshSearch() {
|
||||||
|
if (!active.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const version = ++searchVersion
|
const version = ++searchVersion
|
||||||
debug('refreshSearch start', {
|
|
||||||
version,
|
|
||||||
projectType: options.projectType.value,
|
|
||||||
params: effectiveRequestParams.value.substring(0, 100),
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentHitsEmpty = isServerType.value
|
const currentHitsEmpty = isServerType.value
|
||||||
? serverHits.value.length === 0
|
? serverHits.value.length === 0
|
||||||
@@ -221,8 +231,11 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
|
|||||||
try {
|
try {
|
||||||
const response = await options.search(effectiveRequestParams.value)
|
const response = await options.search(effectiveRequestParams.value)
|
||||||
|
|
||||||
|
if (!active.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (version !== searchVersion) {
|
if (version !== searchVersion) {
|
||||||
debug('refreshSearch stale, discarding', { version, current: searchVersion })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,17 +245,10 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
|
|||||||
projectHits.value = response.projectHits
|
projectHits.value = response.projectHits
|
||||||
}
|
}
|
||||||
totalHits.value = response.total_hits
|
totalHits.value = response.total_hits
|
||||||
debug('refreshSearch complete', {
|
|
||||||
version,
|
|
||||||
hits: response.total_hits,
|
|
||||||
projectHits: response.projectHits.length,
|
|
||||||
serverHits: response.serverHits.length,
|
|
||||||
})
|
|
||||||
|
|
||||||
updateUrlParams()
|
updateUrlParams()
|
||||||
loading.value = false
|
loading.value = false
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debug('refreshSearch error', err)
|
|
||||||
console.error('Browse search error:', err)
|
console.error('Browse search error:', err)
|
||||||
if (version === searchVersion) {
|
if (version === searchVersion) {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@@ -251,7 +257,9 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateUrlParams() {
|
function updateUrlParams() {
|
||||||
debug('updateUrlParams', { path: route.path })
|
if (!active.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const persistentParams: Record<string, string | (string | null)[] | null | undefined> = {}
|
const persistentParams: Record<string, string | (string | null)[] | null | undefined> = {}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(route.query)) {
|
for (const [key, value] of Object.entries(route.query)) {
|
||||||
@@ -292,8 +300,7 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => options.projectType.value,
|
() => options.projectType.value,
|
||||||
(newType, oldType) => {
|
() => {
|
||||||
debug('projectType changed', { from: oldType, to: newType })
|
|
||||||
effectiveCurrentSortType.value =
|
effectiveCurrentSortType.value =
|
||||||
effectiveSortTypes.value.find((sortType) => sortType.name === 'relevance') ??
|
effectiveSortTypes.value.find((sortType) => sortType.name === 'relevance') ??
|
||||||
effectiveSortTypes.value[0]
|
effectiveSortTypes.value[0]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { Component } from 'vue'
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import Admonition from '#ui/components/base/Admonition.vue'
|
|
||||||
import PageHeader from '#ui/components/base/PageHeader.vue'
|
import PageHeader from '#ui/components/base/PageHeader.vue'
|
||||||
import LoaderIcon from '#ui/components/servers/icons/LoaderIcon.vue'
|
import LoaderIcon from '#ui/components/servers/icons/LoaderIcon.vue'
|
||||||
import { useServerImage } from '#ui/composables/servers/use-server-image.ts'
|
import { useServerImage } from '#ui/composables/servers/use-server-image.ts'
|
||||||
@@ -171,8 +170,5 @@ async function handleSelectedProjectsLeaveResult(
|
|||||||
title-class="leading-8"
|
title-class="leading-8"
|
||||||
truncate-title
|
truncate-title
|
||||||
/>
|
/>
|
||||||
<Admonition v-if="installContext.warning" type="warning" class="mb-1">
|
|
||||||
{{ installContext.warning }}
|
|
||||||
</Admonition>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Labrinth } from '@modrinth/api-client'
|
|||||||
import { SearchIcon } from '@modrinth/assets'
|
import { SearchIcon } from '@modrinth/assets'
|
||||||
import { computed, toValue } from 'vue'
|
import { computed, toValue } from 'vue'
|
||||||
|
|
||||||
|
import Admonition from '#ui/components/base/Admonition.vue'
|
||||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||||
import Combobox, { type ComboboxOption } from '#ui/components/base/Combobox.vue'
|
import Combobox, { type ComboboxOption } from '#ui/components/base/Combobox.vue'
|
||||||
import LoadingIndicator from '#ui/components/base/LoadingIndicator.vue'
|
import LoadingIndicator from '#ui/components/base/LoadingIndicator.vue'
|
||||||
@@ -21,6 +22,7 @@ import { injectBrowseManager } from './providers/browse-manager'
|
|||||||
const ctx = injectBrowseManager()
|
const ctx = injectBrowseManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const lockedMessages = computed(() => toValue(ctx.lockedFilterMessages))
|
const lockedMessages = computed(() => toValue(ctx.lockedFilterMessages))
|
||||||
|
const installWarning = computed(() => ctx.installContext?.value?.warning)
|
||||||
|
|
||||||
const sortOptions = computed<ComboboxOption<SortType>[]>(() =>
|
const sortOptions = computed<ComboboxOption<SortType>[]>(() =>
|
||||||
ctx.effectiveSortTypes.value.map((st) => ({
|
ctx.effectiveSortTypes.value.map((st) => ({
|
||||||
@@ -61,6 +63,10 @@ const messages = defineMessages({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<Admonition v-if="installWarning" type="warning">
|
||||||
|
{{ installWarning }}
|
||||||
|
</Admonition>
|
||||||
|
|
||||||
<NavTabs v-if="ctx.showProjectTypeTabs.value" :links="ctx.selectableProjectTypes.value" />
|
<NavTabs v-if="ctx.showProjectTypeTabs.value" :links="ctx.selectableProjectTypes.value" />
|
||||||
|
|
||||||
<StyledInput
|
<StyledInput
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<Admonition
|
<div
|
||||||
v-if="!instanceInfoAdmonitionDismissed"
|
v-if="!instanceInfoAdmonitionDismissed"
|
||||||
type="info"
|
class="grid grid-cols-[1.5rem_minmax(0,1fr)_auto] items-start gap-x-3 rounded-2xl border border-solid border-brand-blue bg-bg-blue p-5 pr-4 text-contrast"
|
||||||
:header="formatMessage(messages.instanceInfoHeader)"
|
|
||||||
dismissible
|
|
||||||
@dismiss="dismissInstanceInfoAdmonition"
|
|
||||||
>
|
>
|
||||||
<ul class="m-0 pl-4">
|
<InfoIcon class="mt-0.5 size-6 text-brand-blue" aria-hidden="true" />
|
||||||
<li>{{ formatMessage(messages.instanceInfoDefinition) }}</li>
|
<div class="flex min-w-0 flex-col gap-1">
|
||||||
<li>{{ formatMessage(messages.instanceInfoSwitching) }}</li>
|
<h2 class="m-0 text-xl font-bold leading-7">
|
||||||
<li>{{ formatMessage(messages.instanceInfoFiles) }}</li>
|
{{ formatMessage(messages.instanceInfoHeader) }}
|
||||||
</ul>
|
</h2>
|
||||||
</Admonition>
|
<p class="m-0 text-lg leading-7 text-contrast/85">
|
||||||
|
{{ formatMessage(messages.instanceInfoBody) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ButtonStyled circular type="transparent" color="blue" hover-color-fill="background">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mt-0.5"
|
||||||
|
:aria-label="formatMessage(messages.instanceInfoDismiss)"
|
||||||
|
@click="dismissInstanceInfoAdmonition"
|
||||||
|
>
|
||||||
|
<XIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="worldsPending"
|
v-if="worldsPending"
|
||||||
@@ -39,12 +50,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Archon } from '@modrinth/api-client'
|
import type { Archon } from '@modrinth/api-client'
|
||||||
|
import { InfoIcon, XIcon } from '@modrinth/assets'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import Admonition from '#ui/components/base/Admonition.vue'
|
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||||
import InstanceCard from '#ui/components/servers/instances/InstanceCard.vue'
|
import InstanceCard from '#ui/components/servers/instances/InstanceCard.vue'
|
||||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
import {
|
import {
|
||||||
@@ -61,20 +73,16 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
instanceInfoHeader: {
|
instanceInfoHeader: {
|
||||||
id: 'servers.manage.instances.info.header',
|
id: 'servers.manage.instances.info.header',
|
||||||
defaultMessage: 'What is an instance?',
|
defaultMessage: 'What is a server instance?',
|
||||||
},
|
},
|
||||||
instanceInfoDefinition: {
|
instanceInfoBody: {
|
||||||
id: 'servers.manage.instances.info.definition',
|
id: 'servers.manage.instances.info.body',
|
||||||
defaultMessage: 'An instance is a separate server setup.',
|
|
||||||
},
|
|
||||||
instanceInfoSwitching: {
|
|
||||||
id: 'servers.manage.instances.info.switching',
|
|
||||||
defaultMessage: 'You can switch which instance your server runs.',
|
|
||||||
},
|
|
||||||
instanceInfoFiles: {
|
|
||||||
id: 'servers.manage.instances.info.files',
|
|
||||||
defaultMessage:
|
defaultMessage:
|
||||||
'Each instance has its own server files, worlds, installed content, and settings.',
|
'An instance is a separate setup of your server with its own content, files, worlds, and settings. You can switch which instance your server runs at any time.',
|
||||||
|
},
|
||||||
|
instanceInfoDismiss: {
|
||||||
|
id: 'servers.manage.instances.info.dismiss',
|
||||||
|
defaultMessage: "Don't show this again",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user