Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 6 additions & 89 deletions apps/app-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
NewsArticleCard,
NotificationPanel,
PopupNotificationPanel,
provideLoadingState,
provideModalBehavior,
provideModrinthClient,
provideNotificationManager,
Expand All @@ -63,6 +64,7 @@ import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
import { computed, nextTick, onMounted, onUnmounted, provide, ref, watch } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'

import { useRouteLoading } from '@/app/runtime/use-route-loading'
import AppTitleBar from '@/app/shell/title-bar/index.vue'
import AccountsCard from '@/components/ui/AccountsCard.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue'
Expand Down Expand Up @@ -184,7 +186,6 @@ import { createServerInstall, provideServerInstall } from '@/providers/server-in
import { setupProviders } from '@/providers/setup'
import { setupAppEventsProvider } from '@/providers/setup/app-events'
import { setupAuthProvider } from '@/providers/setup/auth'
import { setupLoadingStateProvider } from '@/providers/setup/loading-state'
import { setupAppUserPreferencesProvider } from '@/providers/setup/user-preferences.ts'
import { createBreadcrumbManager, provideBreadcrumbManager } from '@/shared/breadcrumbs'
import { appMessages } from '@/utils/app-messages'
Expand All @@ -206,15 +207,6 @@ const { channel: appEventChannel, events: appEvents } = setupAppEventsProvider()
useInstanceMetadataRefresh(appEvents)
const breadcrumbManager = createBreadcrumbManager()
provideBreadcrumbManager(breadcrumbManager)
const canNavigateBack = ref(false)
const canNavigateForward = ref(false)

function updateHistoryNavigationState() {
const historyState = window.history.state
canNavigateBack.value = historyState?.back != null
canNavigateForward.value = historyState?.forward != null
}

let fullscreenAdsWindowHold = false

async function handleFullscreenChange() {
Expand All @@ -234,8 +226,6 @@ async function handleFullscreenChange() {
}
}

updateHistoryNavigationState()

const APP_LEFT_NAV_WIDTH = '4rem'
const APP_SIDEBAR_WIDTH = 300
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
Expand Down Expand Up @@ -894,6 +884,10 @@ async function setupApp() {
}

const stateFailed = ref(false)
const { loading, canNavigateBack, canNavigateForward, onSuspensePending, onSuspenseResolve } =
useRouteLoading({ stateInitialized, stateFailed })
provideLoadingState(loading)

traceStartupStep('Initialize backend state', () => initialize_state(appEventChannel))
.then(() => {
traceStartupStep('Initialize frontend state', setupApp).catch((err) => {
Expand All @@ -913,94 +907,17 @@ const handleClose = async () => {
await getCurrentWindow().close()
}

const loading = setupLoadingStateProvider(() => ({
stateInitialized: stateInitialized.value,
stateFailed: stateFailed.value,
initialStatePending: !!initialLoadToken,
navigationPending: !!routerToken,
routeSuspensePending: !!suspenseToken,
route: route.path,
}))
loading.setEnabled(false)
let initialLoadToken = loading.begin('Initial app state')
let routerToken = null
let suspenseToken = null

let suspensePending = false

const sidebarOverlayScrollbarsOptions = Object.freeze({
overflow: {
x: 'hidden',
y: 'scroll',
},
})

router.beforeEach((to, from) => {
debugStartup('Route navigation started', { to: to.path, from: from.path })
suspensePending = false
if (routerToken) loading.end(routerToken)
routerToken = loading.begin(`Route navigation: ${to.path}`)
})
router.afterEach((to, from, failure) => {
debugStartup('Route navigation settled', { to: to.path, failed: !!failure })
updateHistoryNavigationState()
trackEvent('PageView', {
path: to.path,
fromPath: from.path,
failed: !!failure,
})
setTimeout(() => {
debugStartup('Route loading release check', {
route: to.path,
suspensePending,
stateInitialized: stateInitialized.value,
})
if (!suspensePending && stateInitialized.value) {
if (initialLoadToken) {
loading.end(initialLoadToken)
initialLoadToken = null
}
if (routerToken) {
loading.end(routerToken)
routerToken = null
}
}
}, 100)
})

function onSuspensePending() {
debugStartup('Route Suspense pending', { route: route.path })
suspensePending = true
if (suspenseToken) loading.end(suspenseToken)
suspenseToken = loading.begin(`Route Suspense: ${route.path}`)
}

function onSuspenseResolve() {
debugStartup('Route Suspense resolved', { route: route.path })
if (suspenseToken) {
loading.end(suspenseToken)
suspenseToken = null
}
if (routerToken) {
loading.end(routerToken)
routerToken = null
}
}

const queryClient = useQueryClient()

watch(stateInitialized, (ready) => {
debugStartup('State readiness changed', { ready })
if (ready) {
if (initialLoadToken) {
loading.end(initialLoadToken)
initialLoadToken = null
}
if (routerToken) {
loading.end(routerToken)
routerToken = null
}

queryClient.prefetchQuery({
queryKey: ['servers'],
queryFn: async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
createLoadingStateCore,
type LoadingStateProvider,
provideLoadingState,
} from '@modrinth/ui'
import { createLoadingStateCore, type LoadingStateProvider } from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { getCurrentInstance, onScopeDispose } from 'vue'

Expand All @@ -12,21 +8,11 @@ type AppLoadingStateProvider = Omit<LoadingStateProvider, 'begin'> & {
begin(label?: string): symbol
}

/**
* Source of truth for the desktop app's loading state.
*
* Owns the token-based ref-counter directly. Consumers
* obtain the same reactive state via `injectLoadingState()` from `@modrinth/ui`.
*
* Returns the provider so the call site (App.vue) can also use it directly
* without a second injection round-trip.
*/
export function setupLoadingStateProvider(
export function useAppLoadingState(
getContext: () => Record<string, unknown> = () => ({}),
): AppLoadingStateProvider {
const core = createLoadingStateCore({ barEnabled: false })
if (!import.meta.env.DEV) {
provideLoadingState(core)
return core
}
const queryClient = useQueryClient()
Expand Down Expand Up @@ -114,6 +100,5 @@ export function setupLoadingStateProvider(
},
}
onScopeDispose(stopReporting)
provideLoadingState(provider)
return provider
}
120 changes: 120 additions & 0 deletions apps/app-frontend/src/app/runtime/use-route-loading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { onScopeDispose, type Ref, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'

import { trackEvent } from '@/helpers/analytics'
import { debugStartup } from '@/helpers/startup-debug'

import { useAppLoadingState } from './use-app-loading-state'

type RouteLoadingOptions = {
stateInitialized: Readonly<Ref<boolean>>
stateFailed: Readonly<Ref<boolean>>
}

export function useRouteLoading({ stateInitialized, stateFailed }: RouteLoadingOptions) {
const router = useRouter()
const route = useRoute()
const canNavigateBack = ref(false)
const canNavigateForward = ref(false)
let initialLoadToken: symbol | null = null
let routerToken: symbol | null = null
let suspenseToken: symbol | null = null
let suspensePending = false
const releaseTimers = new Set<ReturnType<typeof setTimeout>>()

const loading = useAppLoadingState(() => ({
stateInitialized: stateInitialized.value,
stateFailed: stateFailed.value,
initialStatePending: !!initialLoadToken,
navigationPending: !!routerToken,
routeSuspensePending: !!suspenseToken,
route: route.path,
}))
loading.setEnabled(false)
initialLoadToken = loading.begin('Initial app state')

function updateHistoryNavigationState() {
const historyState = window.history.state
canNavigateBack.value = historyState?.back != null
canNavigateForward.value = historyState?.forward != null
}

function releaseInitialAndNavigationTokens() {
if (initialLoadToken) {
loading.end(initialLoadToken)
initialLoadToken = null
}
if (routerToken) {
loading.end(routerToken)
routerToken = null
}
}

updateHistoryNavigationState()

const removeBeforeEach = router.beforeEach((to, from) => {
debugStartup('Route navigation started', { to: to.path, from: from.path })
suspensePending = false
if (routerToken) loading.end(routerToken)
routerToken = loading.begin(`Route navigation: ${to.path}`)
})
const removeAfterEach = router.afterEach((to, from, failure) => {
debugStartup('Route navigation settled', { to: to.path, failed: !!failure })
updateHistoryNavigationState()
trackEvent('PageView', {
path: to.path,
fromPath: from.path,
failed: !!failure,
})
const timer = setTimeout(() => {
releaseTimers.delete(timer)
debugStartup('Route loading release check', {
route: to.path,
suspensePending,
stateInitialized: stateInitialized.value,
})
if (!suspensePending && stateInitialized.value) {
releaseInitialAndNavigationTokens()
}
}, 100)
releaseTimers.add(timer)
})

function onSuspensePending() {
debugStartup('Route Suspense pending', { route: route.path })
suspensePending = true
if (suspenseToken) loading.end(suspenseToken)
suspenseToken = loading.begin(`Route Suspense: ${route.path}`)
}

function onSuspenseResolve() {
debugStartup('Route Suspense resolved', { route: route.path })
if (suspenseToken) {
loading.end(suspenseToken)
suspenseToken = null
}
if (routerToken) {
loading.end(routerToken)
routerToken = null
}
}

watch(stateInitialized, (ready) => {
debugStartup('State readiness changed', { ready })
if (ready) releaseInitialAndNavigationTokens()
})

onScopeDispose(() => {
removeBeforeEach()
removeAfterEach()
for (const timer of releaseTimers) clearTimeout(timer)
releaseTimers.clear()
releaseInitialAndNavigationTokens()
if (suspenseToken) {
loading.end(suspenseToken)
suspenseToken = null
}
})

return { loading, canNavigateBack, canNavigateForward, onSuspensePending, onSuspenseResolve }
}
Loading