Skip to content

Commit 176d430

Browse files
authored
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
1 parent 3e32901 commit 176d430

47 files changed

Lines changed: 2047 additions & 1355 deletions

File tree

Some content is hidden

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

.claude/skills/cross-platform-pages/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ Refer to the standards: @standards/frontend/CROSS_PLATFORM_PAGES.md and @standar
2222
- Move the page component into `packages/ui/src/layouts/wrapped/` matching the route structure.
2323
- Replace any platform-specific imports with shared utilities.
2424
- Import and render the wrapped page from both frontends as a simple component.
25+
- If the layout uses TanStack Query for initial route paint with `ReadyTransition` / `useReadyState`, each platform route shell must call `ensureQueryData` for those queries with matching keys and fetchers — see **Platform route shells: prefetch with `ensureQueryData`** in `standards/frontend/CROSS_PLATFORM_PAGES.md`.
2526
6. **Verify** the page renders correctly by checking for missing imports and that all DI contracts are satisfied.

AGENTS.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

apps/app-frontend/src/App.vue

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
CreationFlowModal,
3939
defineMessages,
4040
I18nDebugPanel,
41+
LoadingBar,
4142
NewsArticleCard,
4243
NotificationPanel,
4344
OverflowMenu,
@@ -52,7 +53,7 @@ import {
5253
useVIntl,
5354
} from '@modrinth/ui'
5455
import { formatBytes, renderString } from '@modrinth/utils'
55-
import { useQuery } from '@tanstack/vue-query'
56+
import { useQuery, useQueryClient } from '@tanstack/vue-query'
5657
import { getVersion } from '@tauri-apps/api/app'
5758
import { invoke } from '@tauri-apps/api/core'
5859
import { getCurrentWindow } from '@tauri-apps/api/window'
@@ -65,7 +66,6 @@ import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
6566
import { RouterView, useRoute, useRouter } from 'vue-router'
6667
6768
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
68-
import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
6969
import AccountsCard from '@/components/ui/AccountsCard.vue'
7070
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
7171
import ErrorModal from '@/components/ui/ErrorModal.vue'
@@ -113,8 +113,9 @@ import {
113113
import { createServerInstall, provideServerInstall } from '@/providers/server-install'
114114
import { setupProviders } from '@/providers/setup'
115115
import { setupAuthProvider } from '@/providers/setup/auth'
116+
import { setupLoadingStateProvider } from '@/providers/setup/loading-state'
116117
import { useError } from '@/store/error.js'
117-
import { useLoading, useTheming } from '@/store/state'
118+
import { useTheming } from '@/store/state'
118119
119120
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
120121
import { get_available_capes, get_available_skins } from './helpers/skins'
@@ -420,9 +421,11 @@ const handleClose = async () => {
420421
const router = useRouter()
421422
const route = useRoute()
422423
423-
const loading = useLoading()
424+
const loading = setupLoadingStateProvider()
424425
loading.setEnabled(false)
425-
loading.startLoading()
426+
let initialLoadToken = loading.begin()
427+
let routerToken = null
428+
let suspenseToken = null
426429
427430
let suspensePending = false
428431
@@ -435,7 +438,8 @@ const sidebarOverlayScrollbarsOptions = Object.freeze({
435438
436439
router.beforeEach(() => {
437440
suspensePending = false
438-
loading.startLoading()
441+
if (routerToken) loading.end(routerToken)
442+
routerToken = loading.begin()
439443
})
440444
router.afterEach((to, from, failure) => {
441445
trackEvent('PageView', {
@@ -445,11 +449,83 @@ router.afterEach((to, from, failure) => {
445449
})
446450
setTimeout(() => {
447451
if (!suspensePending && stateInitialized.value) {
448-
loading.stopLoading()
452+
if (initialLoadToken) {
453+
loading.end(initialLoadToken)
454+
initialLoadToken = null
455+
}
456+
if (routerToken) {
457+
loading.end(routerToken)
458+
routerToken = null
459+
}
449460
}
450461
}, 100)
451462
})
452463
464+
function onSuspensePending() {
465+
suspensePending = true
466+
if (suspenseToken) loading.end(suspenseToken)
467+
suspenseToken = loading.begin()
468+
}
469+
470+
function onSuspenseResolve() {
471+
if (suspenseToken) {
472+
loading.end(suspenseToken)
473+
suspenseToken = null
474+
}
475+
if (routerToken) {
476+
loading.end(routerToken)
477+
routerToken = null
478+
}
479+
}
480+
481+
const queryClient = useQueryClient()
482+
483+
watch(stateInitialized, (ready) => {
484+
if (ready) {
485+
if (initialLoadToken) {
486+
loading.end(initialLoadToken)
487+
initialLoadToken = null
488+
}
489+
if (routerToken) {
490+
loading.end(routerToken)
491+
routerToken = null
492+
}
493+
494+
queryClient.prefetchQuery({
495+
queryKey: ['servers'],
496+
queryFn: async () => {
497+
const response = await tauriApiClient.archon.servers_v0.list({ limit: 100 })
498+
const hasMedalServers = response.servers.some((s) => s.is_medal)
499+
if (hasMedalServers) {
500+
const subscriptions = await tauriApiClient.labrinth.billing_internal.getSubscriptions()
501+
for (const server of response.servers) {
502+
if (server.is_medal) {
503+
const sub = subscriptions.find((s) => s.metadata?.id === server.server_id)
504+
if (sub) {
505+
server.medal_expires = new Date(
506+
new Date(sub.created).getTime() + 5 * 86400000,
507+
).toISOString()
508+
}
509+
}
510+
}
511+
}
512+
return response
513+
},
514+
staleTime: 30_000,
515+
})
516+
queryClient.prefetchQuery({
517+
queryKey: ['billing', 'subscriptions'],
518+
queryFn: () => tauriApiClient.labrinth.billing_internal.getSubscriptions(),
519+
staleTime: 30_000,
520+
})
521+
queryClient.prefetchQuery({
522+
queryKey: ['billing', 'payments'],
523+
queryFn: () => tauriApiClient.labrinth.billing_internal.getPayments(),
524+
staleTime: 30_000,
525+
})
526+
}
527+
})
528+
453529
const error = useError()
454530
const errorModal = ref()
455531
const minecraftAuthErrorModal = ref()
@@ -1236,7 +1312,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
12361312
width: 'calc(100% - var(--left-bar-width) - var(--right-bar-width))',
12371313
}"
12381314
>
1239-
<ModrinthLoadingIndicator />
1315+
<LoadingBar position="absolute" />
12401316
</div>
12411317
<div
12421318
v-if="themeStore.featureFlags.page_path"
@@ -1272,19 +1348,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
12721348
</Admonition>
12731349
<RouterView v-slot="{ Component }">
12741350
<template v-if="Component">
1275-
<Suspense
1276-
@pending="
1277-
() => {
1278-
suspensePending = true
1279-
loading.startLoading()
1280-
}
1281-
"
1282-
@resolve="
1283-
() => {
1284-
loading.stopLoading()
1285-
}
1286-
"
1287-
>
1351+
<Suspense @pending="onSuspensePending" @resolve="onSuspenseResolve">
12881352
<component :is="Component"></component>
12891353
</Suspense>
12901354
</template>

apps/app-frontend/src/components/LoadingIndicatorBar.vue

Lines changed: 0 additions & 142 deletions
This file was deleted.

apps/app-frontend/src/components/ui/SplashScreen.vue

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@
7878
</template>
7979

8080
<script setup>
81+
import { injectLoadingState } from '@modrinth/ui'
8182
import { ref, watch } from 'vue'
8283
8384
import ProgressBar from '@/components/ui/ProgressBar.vue'
8485
import { loading_listener } from '@/helpers/events.js'
85-
import { useLoading } from '@/store/loading.js'
8686
8787
const doneLoading = ref(false)
8888
const loadingProgress = ref(0)
@@ -91,20 +91,20 @@ const message = ref()
9191
const MIN_DISPLAY_MS = 500
9292
const mountedAt = Date.now()
9393
94-
const loading = useLoading()
94+
const loading = injectLoadingState()
9595
9696
function onAfterLeave() {
9797
loading.setEnabled(true)
9898
}
9999
100100
watch(
101-
loading,
102-
(newValue) => {
103-
if (newValue.barEnabled) {
101+
[loading.barEnabled, loading.pending],
102+
([barEnabled, pending]) => {
103+
if (barEnabled) {
104104
return
105105
}
106106
107-
if (loading.loading) {
107+
if (pending) {
108108
loadingProgress.value = 0
109109
fakeLoadingIncrease()
110110
return
@@ -114,7 +114,7 @@ watch(
114114
const delay = Math.max(0, MIN_DISPLAY_MS - elapsed)
115115
116116
setTimeout(() => {
117-
if (loading.loading) {
117+
if (loading.pending.value) {
118118
return
119119
}
120120
doneLoading.value = true

0 commit comments

Comments
 (0)