feat: shared loading state + cleanup loading state management (#5835)
* feat: implement shared loading bar component and polished loading states across the app * feat: align loading states + ensureQueryData changes * fix: lint + bugs * fix: skeleton for manage servers page * fix: merge conflict fix
This commit is contained in:
@@ -1,7 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { injectModrinthServerContext, ServersManageBackupsPage } from '@modrinth/ui'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
ServersManageBackupsPage,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
const { isServerRunning } = injectModrinthServerContext()
|
||||
const client = injectModrinthClient()
|
||||
const { serverId, worldId, isServerRunning } = injectModrinthServerContext()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
if (worldId.value) {
|
||||
try {
|
||||
await queryClient.ensureQueryData({
|
||||
queryKey: ['backups', 'list', serverId],
|
||||
queryFn: () => client.archon.backups_v1.list(serverId, worldId.value!),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
} catch {
|
||||
// Let mounted layouts' useQuery surface errors; do not fail route setup.
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { ServersManageContentPage } from '@modrinth/ui'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
ServersManageContentPage,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { serverId, worldId } = injectModrinthServerContext()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
if (worldId.value) {
|
||||
try {
|
||||
await queryClient.ensureQueryData({
|
||||
queryKey: ['content', 'list', 'v1', serverId],
|
||||
queryFn: () =>
|
||||
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
} catch {
|
||||
// Let mounted layouts' useQuery surface errors; do not fail route setup.
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { ServersManageFilesPage } from '@modrinth/ui'
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
ServersManageFilesPage,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { serverId } = injectModrinthServerContext()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
try {
|
||||
await queryClient.ensureQueryData({
|
||||
queryKey: ['files', serverId, '/'],
|
||||
queryFn: () => client.kyros.files_v0.listDirectory('/', 1, 2000),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
} catch {
|
||||
// Let mounted layouts' useQuery surface errors; do not fail route setup.
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -35,9 +35,6 @@
|
||||
@reinstall="onReinstall"
|
||||
@reinstall-failed="onReinstallFailed"
|
||||
/>
|
||||
<template #fallback>
|
||||
<LoadingIndicator />
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
</RouterView>
|
||||
@@ -48,8 +45,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import { injectAuth, LoadingIndicator, ServersManageRootLayout } from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { injectAuth, injectModrinthClient, ServersManageRootLayout } from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, watch } from 'vue'
|
||||
@@ -64,6 +61,8 @@ import { useTheming } from '@/store/theme'
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = injectAuth()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const themeStore = useTheming()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
@@ -72,6 +71,18 @@ const serverId = computed(() => {
|
||||
return Array.isArray(rawId) ? rawId[0] : (rawId ?? '')
|
||||
})
|
||||
|
||||
if (serverId.value) {
|
||||
try {
|
||||
await queryClient.ensureQueryData({
|
||||
queryKey: ['servers', 'detail', serverId.value],
|
||||
queryFn: () => client.archon.servers_v0.get(serverId.value)!,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
} catch {
|
||||
// Let mounted layouts' useQuery surface errors; do not fail route setup.
|
||||
}
|
||||
}
|
||||
|
||||
const { data: serverData } = useQuery({
|
||||
queryKey: computed(() => ['servers', 'detail', serverId.value]),
|
||||
queryFn: () => null as unknown as Archon.Servers.v0.Server,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FilePageLayout,
|
||||
injectNotificationManager,
|
||||
provideFileManager,
|
||||
ReadyTransition,
|
||||
useDebugLogger,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
@@ -54,6 +55,8 @@ const messages = defineMessages({
|
||||
|
||||
const instanceRoot = ref('')
|
||||
const items = ref<FileItem[]>([])
|
||||
/** True until the first directory read for the current instance path finishes (initial load only). */
|
||||
const firstPaintPending = ref(true)
|
||||
const loading = ref(true)
|
||||
const error = ref<Error | null>(null)
|
||||
const currentPath = ref('')
|
||||
@@ -123,6 +126,7 @@ async function refresh() {
|
||||
items.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
firstPaintPending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +309,7 @@ watch(
|
||||
() => props.instance.path,
|
||||
async () => {
|
||||
debug('watch instance.path: changed to', props.instance.path)
|
||||
firstPaintPending.value = true
|
||||
instanceRoot.value = await get_full_path(props.instance.path)
|
||||
currentPath.value = ''
|
||||
await refresh()
|
||||
@@ -341,5 +346,7 @@ provideFileManager({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FilePageLayout :show-refresh-button="true" />
|
||||
<ReadyTransition :pending="firstPaintPending">
|
||||
<FilePageLayout :show-refresh-button="true" />
|
||||
</ReadyTransition>
|
||||
</template>
|
||||
|
||||
@@ -218,11 +218,7 @@
|
||||
:key="instance.path"
|
||||
>
|
||||
<template v-if="Component">
|
||||
<Suspense
|
||||
:key="instance.path"
|
||||
@pending="loadingBar.startLoading()"
|
||||
@resolve="loadingBar.stopLoading()"
|
||||
>
|
||||
<Suspense :key="instance.path">
|
||||
<component
|
||||
:is="Component"
|
||||
:instance="instance"
|
||||
@@ -235,9 +231,6 @@
|
||||
@play="updatePlayState"
|
||||
@stop="() => stopInstance('InstanceSubpage')"
|
||||
></component>
|
||||
<template #fallback>
|
||||
<LoadingIndicator />
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
</RouterView>
|
||||
@@ -296,7 +289,6 @@ import {
|
||||
ButtonStyled,
|
||||
ContentPageHeader,
|
||||
injectNotificationManager,
|
||||
LoadingIndicator,
|
||||
NavTabs,
|
||||
OverflowMenu,
|
||||
ServerOnlinePlayers,
|
||||
@@ -304,6 +296,7 @@ import {
|
||||
ServerRecentPlays,
|
||||
ServerRegion,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
import duration from 'dayjs/plugin/duration'
|
||||
@@ -323,16 +316,17 @@ import { get_by_profile_path } from '@/helpers/process'
|
||||
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { get_server_status } from '@/helpers/worlds'
|
||||
import { get_server_status, refreshWorlds } from '@/helpers/worlds'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { useBreadcrumbs, useLoading } from '@/store/state'
|
||||
import { useBreadcrumbs } from '@/store/state'
|
||||
|
||||
dayjs.extend(duration)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { playServerProject } = injectServerInstall()
|
||||
const queryClient = useQueryClient()
|
||||
const route = useRoute()
|
||||
|
||||
const router = useRouter()
|
||||
@@ -392,6 +386,14 @@ async function fetchInstance() {
|
||||
}
|
||||
|
||||
fetchDeferredData()
|
||||
|
||||
if (instance.value) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['worlds', instance.value.path],
|
||||
queryFn: () => refreshWorlds(instance.value!.path),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function fetchDeferredData() {
|
||||
@@ -471,8 +473,6 @@ if (instance.value) {
|
||||
})
|
||||
}
|
||||
|
||||
const loadingBar = useLoading()
|
||||
|
||||
const options = ref<InstanceType<typeof ContextMenu> | null>(null)
|
||||
|
||||
const startInstance = async (context: string) => {
|
||||
|
||||
@@ -1,65 +1,67 @@
|
||||
<template>
|
||||
<ContentPageLayout>
|
||||
<template #modals>
|
||||
<ShareModalWrapper
|
||||
ref="shareModal"
|
||||
:share-title="formatMessage(messages.shareTitle)"
|
||||
:share-text="formatMessage(messages.shareText)"
|
||||
:open-in-new-tab="false"
|
||||
/>
|
||||
<ModpackContentModal
|
||||
ref="modpackContentModal"
|
||||
:modpack-name="linkedModpackProject?.title"
|
||||
:modpack-icon-url="linkedModpackProject?.icon_url ?? undefined"
|
||||
:enable-toggle="!props.isServerInstance"
|
||||
:get-overflow-options="getOverflowOptions"
|
||||
:switch-version="handleSwitchVersion"
|
||||
@update:enabled="handleModpackContentToggle"
|
||||
@bulk:enable="handleModpackContentBulkToggle"
|
||||
@bulk:disable="handleModpackContentBulkToggle"
|
||||
/>
|
||||
<ConfirmModpackUpdateModal
|
||||
ref="modpackUpdateConfirmModal"
|
||||
:downgrade="isModpackUpdateDowngrade"
|
||||
:backup-tip="
|
||||
[linkedModpackProject?.title, pendingModpackUpdateVersion?.version_number]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
"
|
||||
@confirm="handleModpackUpdateConfirm"
|
||||
@cancel="handleModpackUpdateCancel"
|
||||
/>
|
||||
<ExportModal v-if="projects.length > 0" ref="exportModal" :instance="instance" />
|
||||
<ContentUpdaterModal
|
||||
v-if="updatingProject || updatingModpack"
|
||||
ref="contentUpdaterModal"
|
||||
:versions="updatingProjectVersions"
|
||||
:current-game-version="instance.game_version"
|
||||
:current-loader="instance.loader"
|
||||
:current-version-id="
|
||||
updatingModpack
|
||||
? (instance.linked_data?.version_id ?? '')
|
||||
: (updatingProject?.version?.id ?? '')
|
||||
"
|
||||
:is-app="true"
|
||||
:project-type="updatingModpack ? 'modpack' : updatingProject?.project_type"
|
||||
:project-icon-url="
|
||||
updatingModpack ? linkedModpackProject?.icon_url : updatingProject?.project?.icon_url
|
||||
"
|
||||
:project-name="
|
||||
updatingModpack
|
||||
? (linkedModpackProject?.title ?? formatMessage(commonMessages.modpackLabel))
|
||||
: (updatingProject?.project?.title ?? updatingProject?.file_name)
|
||||
"
|
||||
:loading="loadingVersions"
|
||||
:loading-changelog="loadingChangelog"
|
||||
@update="handleModalUpdate"
|
||||
@cancel="resetUpdateState"
|
||||
@version-select="handleVersionSelect"
|
||||
@version-hover="handleVersionHover"
|
||||
/>
|
||||
</template>
|
||||
</ContentPageLayout>
|
||||
<ReadyTransition :pending="loading">
|
||||
<ContentPageLayout>
|
||||
<template #modals>
|
||||
<ShareModalWrapper
|
||||
ref="shareModal"
|
||||
:share-title="formatMessage(messages.shareTitle)"
|
||||
:share-text="formatMessage(messages.shareText)"
|
||||
:open-in-new-tab="false"
|
||||
/>
|
||||
<ModpackContentModal
|
||||
ref="modpackContentModal"
|
||||
:modpack-name="linkedModpackProject?.title"
|
||||
:modpack-icon-url="linkedModpackProject?.icon_url ?? undefined"
|
||||
:enable-toggle="!props.isServerInstance"
|
||||
:get-overflow-options="getOverflowOptions"
|
||||
:switch-version="handleSwitchVersion"
|
||||
@update:enabled="handleModpackContentToggle"
|
||||
@bulk:enable="handleModpackContentBulkToggle"
|
||||
@bulk:disable="handleModpackContentBulkToggle"
|
||||
/>
|
||||
<ConfirmModpackUpdateModal
|
||||
ref="modpackUpdateConfirmModal"
|
||||
:downgrade="isModpackUpdateDowngrade"
|
||||
:backup-tip="
|
||||
[linkedModpackProject?.title, pendingModpackUpdateVersion?.version_number]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
"
|
||||
@confirm="handleModpackUpdateConfirm"
|
||||
@cancel="handleModpackUpdateCancel"
|
||||
/>
|
||||
<ExportModal v-if="projects.length > 0" ref="exportModal" :instance="instance" />
|
||||
<ContentUpdaterModal
|
||||
v-if="updatingProject || updatingModpack"
|
||||
ref="contentUpdaterModal"
|
||||
:versions="updatingProjectVersions"
|
||||
:current-game-version="instance.game_version"
|
||||
:current-loader="instance.loader"
|
||||
:current-version-id="
|
||||
updatingModpack
|
||||
? (instance.linked_data?.version_id ?? '')
|
||||
: (updatingProject?.version?.id ?? '')
|
||||
"
|
||||
:is-app="true"
|
||||
:project-type="updatingModpack ? 'modpack' : updatingProject?.project_type"
|
||||
:project-icon-url="
|
||||
updatingModpack ? linkedModpackProject?.icon_url : updatingProject?.project?.icon_url
|
||||
"
|
||||
:project-name="
|
||||
updatingModpack
|
||||
? (linkedModpackProject?.title ?? formatMessage(commonMessages.modpackLabel))
|
||||
: (updatingProject?.project?.title ?? updatingProject?.file_name)
|
||||
"
|
||||
:loading="loadingVersions"
|
||||
:loading-changelog="loadingChangelog"
|
||||
@update="handleModalUpdate"
|
||||
@cancel="resetUpdateState"
|
||||
@version-select="handleVersionSelect"
|
||||
@version-hover="handleVersionHover"
|
||||
/>
|
||||
</template>
|
||||
</ContentPageLayout>
|
||||
</ReadyTransition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -82,6 +84,7 @@ import {
|
||||
type OverflowMenuOption,
|
||||
provideAppBackup,
|
||||
provideContentManager,
|
||||
ReadyTransition,
|
||||
useDebugLogger,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
@@ -37,22 +37,109 @@
|
||||
:description="formatMessage(messages.deleteWorldDescription, { name: worldToDelete?.name })"
|
||||
@proceed="proceedDeleteWorld"
|
||||
/>
|
||||
<div v-if="dedupedWorlds.length > 0" class="flex flex-col gap-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<StyledInput
|
||||
v-model="searchFilter"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:spellcheck="false"
|
||||
input-class="!h-10"
|
||||
wrapper-class="flex-1 min-w-0"
|
||||
clearable
|
||||
:placeholder="
|
||||
formatMessage(messages.searchWorldsPlaceholder, { count: dedupedWorlds.length })
|
||||
"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<ReadyTransition :pending="worldsReadyPending">
|
||||
<div v-if="dedupedWorlds.length > 0" class="flex flex-col gap-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<StyledInput
|
||||
v-model="searchFilter"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:spellcheck="false"
|
||||
input-class="!h-10"
|
||||
wrapper-class="flex-1 min-w-0"
|
||||
clearable
|
||||
:placeholder="
|
||||
formatMessage(messages.searchWorldsPlaceholder, { count: dedupedWorlds.length })
|
||||
"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!h-10 !border-button-bg !border-[1px]" @click="addServerModal?.show()">
|
||||
<PlusIcon class="size-5" />
|
||||
{{ formatMessage(messages.addServer) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="!h-10 flex items-center gap-2"
|
||||
@click="
|
||||
router.push({ path: '/browse/server', query: { i: instance.path, from: 'worlds' } })
|
||||
"
|
||||
>
|
||||
<CompassIcon class="size-5" />
|
||||
<span>{{ formatMessage(messages.browseServers) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<FilterIcon class="size-5 text-secondary" />
|
||||
<button
|
||||
:class="filterPillClass(selectedFilters.length === 0)"
|
||||
@click="selectedFilters = []"
|
||||
>
|
||||
{{ formatMessage(commonMessages.allProjectType) }}
|
||||
</button>
|
||||
<button
|
||||
v-for="option in filterOptions"
|
||||
:key="option.id"
|
||||
:class="filterPillClass(selectedFilters.includes(option.id))"
|
||||
@click="toggleFilter(option.id)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
<ButtonStyled type="transparent" hover-color-fill="none">
|
||||
<button :disabled="refreshingAll" @click="refreshAllWorlds">
|
||||
<RefreshCwIcon :class="refreshingAll ? 'animate-spin' : ''" />
|
||||
{{ formatMessage(commonMessages.refreshButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<WorldItem
|
||||
v-for="world in filteredWorlds"
|
||||
:key="`world-${world.type}-${world.type == 'singleplayer' ? world.path : `${world.address}-${world.index}`}`"
|
||||
:world="world"
|
||||
:managed="world.type === 'server' ? isManagedServerWorld(world) : false"
|
||||
:highlighted="highlightedWorld === getWorldIdentifier(world)"
|
||||
:supports-server-quick-play="supportsServerQuickPlay"
|
||||
:supports-world-quick-play="supportsWorldQuickPlay"
|
||||
:current-protocol="protocolVersion"
|
||||
:playing-instance="playing"
|
||||
:playing-world="worldsMatch(world, worldPlaying)"
|
||||
:starting-instance="startingInstance"
|
||||
:refreshing="world.type === 'server' ? serverData[world.address]?.refreshing : undefined"
|
||||
:server-status="world.type === 'server' ? serverData[world.address]?.status : undefined"
|
||||
:rendered-motd="
|
||||
world.type === 'server' ? serverData[world.address]?.renderedMotd : undefined
|
||||
"
|
||||
:game-mode="world.type === 'singleplayer' ? GAME_MODES[world.game_mode] : undefined"
|
||||
@play="() => joinWorld(world)"
|
||||
@stop="() => emit('stop')"
|
||||
@refresh="() => refreshServer((world as ServerWorld).address)"
|
||||
@edit="
|
||||
() =>
|
||||
world.type === 'singleplayer'
|
||||
? editWorldModal?.show(world)
|
||||
: isManagedServerWorld(world)
|
||||
? undefined
|
||||
: editServerModal?.show(world)
|
||||
"
|
||||
@delete="() => !isManagedServerWorld(world) && promptToRemoveWorld(world)"
|
||||
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else
|
||||
type="empty-inbox"
|
||||
:heading="formatMessage(messages.noWorldsHeading)"
|
||||
:description="formatMessage(messages.noWorldsDescription)"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!h-10 !border-button-bg !border-[1px]" @click="addServerModal?.show()">
|
||||
<PlusIcon class="size-5" />
|
||||
@@ -70,94 +157,9 @@
|
||||
<span>{{ formatMessage(messages.browseServers) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<FilterIcon class="size-5 text-secondary" />
|
||||
<button
|
||||
:class="filterPillClass(selectedFilters.length === 0)"
|
||||
@click="selectedFilters = []"
|
||||
>
|
||||
{{ formatMessage(commonMessages.allProjectType) }}
|
||||
</button>
|
||||
<button
|
||||
v-for="option in filterOptions"
|
||||
:key="option.id"
|
||||
:class="filterPillClass(selectedFilters.includes(option.id))"
|
||||
@click="toggleFilter(option.id)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
<ButtonStyled type="transparent" hover-color-fill="none">
|
||||
<button :disabled="refreshingAll" @click="refreshAllWorlds">
|
||||
<RefreshCwIcon :class="refreshingAll ? 'animate-spin' : ''" />
|
||||
{{ formatMessage(commonMessages.refreshButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<WorldItem
|
||||
v-for="world in filteredWorlds"
|
||||
:key="`world-${world.type}-${world.type == 'singleplayer' ? world.path : `${world.address}-${world.index}`}`"
|
||||
:world="world"
|
||||
:managed="world.type === 'server' ? isManagedServerWorld(world) : false"
|
||||
:highlighted="highlightedWorld === getWorldIdentifier(world)"
|
||||
:supports-server-quick-play="supportsServerQuickPlay"
|
||||
:supports-world-quick-play="supportsWorldQuickPlay"
|
||||
:current-protocol="protocolVersion"
|
||||
:playing-instance="playing"
|
||||
:playing-world="worldsMatch(world, worldPlaying)"
|
||||
:starting-instance="startingInstance"
|
||||
:refreshing="world.type === 'server' ? serverData[world.address]?.refreshing : undefined"
|
||||
:server-status="world.type === 'server' ? serverData[world.address]?.status : undefined"
|
||||
:rendered-motd="
|
||||
world.type === 'server' ? serverData[world.address]?.renderedMotd : undefined
|
||||
"
|
||||
:game-mode="world.type === 'singleplayer' ? GAME_MODES[world.game_mode] : undefined"
|
||||
@play="() => joinWorld(world)"
|
||||
@stop="() => emit('stop')"
|
||||
@refresh="() => refreshServer((world as ServerWorld).address)"
|
||||
@edit="
|
||||
() =>
|
||||
world.type === 'singleplayer'
|
||||
? editWorldModal?.show(world)
|
||||
: isManagedServerWorld(world)
|
||||
? undefined
|
||||
: editServerModal?.show(world)
|
||||
"
|
||||
@delete="() => !isManagedServerWorld(world) && promptToRemoveWorld(world)"
|
||||
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else
|
||||
type="empty-inbox"
|
||||
:heading="formatMessage(messages.noWorldsHeading)"
|
||||
:description="formatMessage(messages.noWorldsDescription)"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!h-10 !border-button-bg !border-[1px]" @click="addServerModal?.show()">
|
||||
<PlusIcon class="size-5" />
|
||||
{{ formatMessage(messages.addServer) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="!h-10 flex items-center gap-2"
|
||||
@click="
|
||||
router.push({ path: '/browse/server', query: { i: instance.path, from: 'worlds' } })
|
||||
"
|
||||
>
|
||||
<CompassIcon class="size-5" />
|
||||
<span>{{ formatMessage(messages.browseServers) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</EmptyState>
|
||||
</template>
|
||||
</EmptyState>
|
||||
</ReadyTransition>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { CompassIcon, FilterIcon, PlusIcon, RefreshCwIcon, SearchIcon } from '@modrinth/assets'
|
||||
@@ -169,11 +171,14 @@ import {
|
||||
GAME_MODES,
|
||||
type GameVersion,
|
||||
injectNotificationManager,
|
||||
ReadyTransition,
|
||||
StyledInput,
|
||||
useReadyState,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import type ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
@@ -344,11 +349,21 @@ function toggleFilter(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const refreshingAll = ref(false)
|
||||
const hadNoWorlds = ref(true)
|
||||
const startingInstance = ref(false)
|
||||
const worldPlaying = ref<World>()
|
||||
|
||||
const worldsQuery = useQuery({
|
||||
queryKey: computed(() => ['worlds', instance.value.path]),
|
||||
queryFn: () => refreshWorlds(instance.value.path),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const worldsReadyPending = useReadyState(worldsQuery)
|
||||
|
||||
const worlds = ref<World[]>([])
|
||||
const serverData = ref<Record<string, ServerData>>({})
|
||||
|
||||
@@ -358,6 +373,26 @@ const isLinux = platform() === 'linux'
|
||||
const linuxRefreshCount = ref(0)
|
||||
|
||||
const protocolVersion = ref<ProtocolVersion | null>(null)
|
||||
|
||||
const gameVersions = ref<GameVersion[]>([])
|
||||
const supportsServerQuickPlay = computed(() =>
|
||||
hasServerQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||
)
|
||||
const supportsWorldQuickPlay = computed(() =>
|
||||
hasWorldQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||
)
|
||||
|
||||
watch(
|
||||
() => worldsQuery.data.value,
|
||||
(data) => {
|
||||
if (data) {
|
||||
worlds.value = [...data]
|
||||
refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
||||
hadNoWorlds.value = worlds.value.length === 0
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
const managedServerName = ref<string | null>(null)
|
||||
const managedServerAddress = ref<string | null>(null)
|
||||
|
||||
@@ -385,8 +420,8 @@ async function refreshManagedServerMetadata() {
|
||||
|
||||
try {
|
||||
const [project, projectV3] = await Promise.all([
|
||||
get_project(projectId, 'bypass'),
|
||||
get_project_v3(projectId, 'bypass'),
|
||||
get_project(projectId),
|
||||
get_project_v3(projectId),
|
||||
])
|
||||
|
||||
if (projectV3?.minecraft_server == null) {
|
||||
@@ -422,27 +457,40 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const [unlistenProfile, , resolvedProtocolVersion, resolvedGameVersions] = await Promise.all([
|
||||
profile_listener(async (e: ProfileEvent) => {
|
||||
if (e.profile_path_id !== instance.value.path) return
|
||||
let unlistenProfile: (() => void) | null = null
|
||||
let worldsTabAlive = true
|
||||
|
||||
console.info(`Handling profile event '${e.event}' for profile: ${e.profile_path_id}`)
|
||||
async function initWorldsTab() {
|
||||
const [_unlistenProfile, resolvedProtocolVersion, resolvedGameVersions] = await Promise.all([
|
||||
profile_listener(async (e: ProfileEvent) => {
|
||||
if (e.profile_path_id !== instance.value.path) return
|
||||
|
||||
if (e.event === 'servers_updated') {
|
||||
if (isLinux && linuxRefreshCount.value >= MAX_LINUX_REFRESHES) return
|
||||
if (isLinux) linuxRefreshCount.value++
|
||||
console.info(`Handling profile event '${e.event}' for profile: ${e.profile_path_id}`)
|
||||
|
||||
await refreshAllWorlds()
|
||||
}
|
||||
if (e.event === 'servers_updated') {
|
||||
if (isLinux && linuxRefreshCount.value >= MAX_LINUX_REFRESHES) return
|
||||
if (isLinux) linuxRefreshCount.value++
|
||||
|
||||
await handleDefaultProfileUpdateEvent(worlds.value, instance.value.path, e)
|
||||
}),
|
||||
refreshAllWorlds(),
|
||||
get_profile_protocol_version(instance.value.path).catch(() => null),
|
||||
get_game_versions().catch(() => [] as GameVersion[]),
|
||||
])
|
||||
await refreshAllWorlds()
|
||||
}
|
||||
|
||||
protocolVersion.value = resolvedProtocolVersion
|
||||
await handleDefaultProfileUpdateEvent(worlds.value, instance.value.path, e)
|
||||
}),
|
||||
get_profile_protocol_version(instance.value.path).catch(() => null),
|
||||
get_game_versions().catch(() => [] as GameVersion[]),
|
||||
])
|
||||
|
||||
if (!worldsTabAlive) {
|
||||
_unlistenProfile()
|
||||
return
|
||||
}
|
||||
|
||||
unlistenProfile = _unlistenProfile
|
||||
protocolVersion.value = resolvedProtocolVersion
|
||||
gameVersions.value = resolvedGameVersions
|
||||
}
|
||||
|
||||
await initWorldsTab()
|
||||
|
||||
async function refreshServer(address: string) {
|
||||
if (!serverData.value[address]) {
|
||||
@@ -458,26 +506,10 @@ async function refreshAllWorlds() {
|
||||
console.log(`Already refreshing, cancelling refresh.`)
|
||||
return
|
||||
}
|
||||
await refreshManagedServerMetadata()
|
||||
|
||||
refreshingAll.value = true
|
||||
|
||||
worlds.value = await refreshWorlds(instance.value.path).finally(
|
||||
() => (refreshingAll.value = false),
|
||||
)
|
||||
refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
||||
|
||||
const hasNoWorlds = worlds.value.length === 0
|
||||
|
||||
if (hadNoWorlds.value && hasNoWorlds) {
|
||||
setTimeout(() => {
|
||||
refreshingAll.value = false
|
||||
}, 1000)
|
||||
} else {
|
||||
refreshingAll.value = false
|
||||
}
|
||||
|
||||
hadNoWorlds.value = hasNoWorlds
|
||||
await queryClient.invalidateQueries({ queryKey: ['worlds', instance.value.path] })
|
||||
refreshingAll.value = false
|
||||
}
|
||||
|
||||
async function addServer(server: ServerWorld) {
|
||||
@@ -592,14 +624,6 @@ function worldsMatch(world: World, other: World | undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
const gameVersions = ref<GameVersion[]>(resolvedGameVersions)
|
||||
const supportsServerQuickPlay = computed(() =>
|
||||
hasServerQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||
)
|
||||
const supportsWorldQuickPlay = computed(() =>
|
||||
hasWorldQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||
)
|
||||
|
||||
const dedupedWorlds = computed(() => {
|
||||
const visibleWorlds: World[] = []
|
||||
const serverIndexByDomain = new Map<string, number>()
|
||||
@@ -749,7 +773,8 @@ async function proceedDeleteWorld() {
|
||||
worldToDelete.value = undefined
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProfile()
|
||||
onBeforeUnmount(() => {
|
||||
worldsTabAlive = false
|
||||
unlistenProfile?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user