mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 12:05:53 +00:00
refactor: breadcrumbs system
This commit is contained in:
@@ -138,6 +138,10 @@ import {
|
||||
setAppUpdateActions,
|
||||
} from '@/providers/app-update.ts'
|
||||
import { createContentInstall, provideContentInstall } from '@/providers/content-install'
|
||||
import {
|
||||
createBreadcrumbManager,
|
||||
provideBreadcrumbManager,
|
||||
} from '@/providers/breadcrumbs'
|
||||
import {
|
||||
provideAppUpdateDownloadProgress,
|
||||
subscribeToDownloadProgress,
|
||||
@@ -157,6 +161,8 @@ import { AppPopupNotificationManager } from './providers/app-popup-notifications
|
||||
const themeStore = useTheming()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const breadcrumbManager = createBreadcrumbManager()
|
||||
provideBreadcrumbManager(breadcrumbManager)
|
||||
const APP_LEFT_NAV_WIDTH = '4rem'
|
||||
const APP_SIDEBAR_WIDTH = 300
|
||||
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
|
||||
|
||||
@@ -15,26 +15,26 @@
|
||||
:class="{ 'breadcrumbs-scroll': isAnimating }"
|
||||
@animationiteration="onAnimationIteration"
|
||||
>
|
||||
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
||||
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
|
||||
<template v-for="(breadcrumb, index) in breadcrumbs" :key="breadcrumb.slot">
|
||||
<router-link
|
||||
v-if="breadcrumb.link"
|
||||
:to="{
|
||||
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id as string)),
|
||||
query: breadcrumb.query,
|
||||
}"
|
||||
v-if="index < breadcrumbs.length - 1 && breadcrumb.to"
|
||||
:to="breadcrumb.to"
|
||||
class="shrink-0 whitespace-nowrap text-primary"
|
||||
>
|
||||
{{ resolveLabel(breadcrumb.name) }}
|
||||
{{ breadcrumb.label }}
|
||||
</router-link>
|
||||
<span
|
||||
v-else
|
||||
data-tauri-drag-region
|
||||
class="shrink-0 whitespace-nowrap text-contrast font-semibold cursor-default select-none"
|
||||
>
|
||||
{{ resolveLabel(breadcrumb.name) }}
|
||||
{{ breadcrumb.label }}
|
||||
</span>
|
||||
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5 shrink-0" />
|
||||
<ChevronRightIcon
|
||||
v-if="index < breadcrumbs.length - 1"
|
||||
data-tauri-drag-region
|
||||
class="w-5 h-5 shrink-0"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,34 +42,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon } from '@modrinth/assets'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { injectBreadcrumbManager } from '@/providers/breadcrumbs'
|
||||
|
||||
interface Breadcrumb {
|
||||
name: string
|
||||
link?: string
|
||||
query?: Record<string, string>
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbData = useBreadcrumbs()
|
||||
|
||||
const breadcrumbs = computed<Breadcrumb[]>(() => {
|
||||
const additionalContext =
|
||||
route.meta.useContext === true
|
||||
? breadcrumbData.context
|
||||
: route.meta.useRootContext === true
|
||||
? breadcrumbData.rootContext
|
||||
: null
|
||||
const crumbs = (route.meta.breadcrumb ?? []) as Breadcrumb[]
|
||||
return additionalContext ? [additionalContext as Breadcrumb, ...crumbs] : crumbs
|
||||
})
|
||||
|
||||
function resolveLabel(name: string): string {
|
||||
return name.charAt(0) === '?' ? breadcrumbData.getName(name.slice(1)) : name
|
||||
}
|
||||
const { entries: breadcrumbs } = injectBreadcrumbManager()
|
||||
|
||||
// Overflow detection
|
||||
const outerRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
@@ -31,9 +31,9 @@ import {
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import { useAppServerBrowse } from '@/composables/browse/use-app-server-browse'
|
||||
@@ -58,7 +58,10 @@ import {
|
||||
createServerInstallContent,
|
||||
provideServerInstallContent,
|
||||
} from '@/providers/setup/server-install-content'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import {
|
||||
useBreadcrumb,
|
||||
useRootBreadcrumb,
|
||||
} from '@/providers/breadcrumbs'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
@@ -71,6 +74,16 @@ const debugLog = useDebugLogger('Browse')
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const displayedBrowseRoute = shallowRef(router.currentRoute.value)
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
(nextRoute) => {
|
||||
if (nextRoute.path.startsWith('/browse/')) {
|
||||
displayedBrowseRoute.value = nextRoute
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
const themeStore = useTheming()
|
||||
const browseRouteActive = computed(() => route.path.startsWith('/browse/'))
|
||||
const serverSetupModalRef = ref<InstanceType<typeof CreationFlowModal> | null>(null)
|
||||
@@ -111,25 +124,6 @@ const {
|
||||
markServerProjectInstalled,
|
||||
} = serverInstallContent
|
||||
|
||||
debugLog('fetching tags (categories, loaders, gameVersions)')
|
||||
const [categories, loaders, availableGameVersions] = await Promise.all([
|
||||
get_categories()
|
||||
.catch(handleError)
|
||||
.then(ref<Labrinth.Tags.v2.Category[]>),
|
||||
get_loaders()
|
||||
.catch(handleError)
|
||||
.then(ref<Labrinth.Tags.v2.Loader[]>),
|
||||
get_game_versions()
|
||||
.catch(handleError)
|
||||
.then(ref<Labrinth.Tags.v2.GameVersion[]>),
|
||||
])
|
||||
|
||||
const tags: Ref<Tags> = computed(() => ({
|
||||
gameVersions: availableGameVersions.value ?? [],
|
||||
loaders: loaders.value ?? [],
|
||||
categories: categories.value ?? [],
|
||||
}))
|
||||
|
||||
type Instance = {
|
||||
game_version: string
|
||||
loader: string
|
||||
@@ -152,6 +146,92 @@ const hiddenInstanceProjectIds = ref<Set<string>>(new Set())
|
||||
const hiddenInstanceProjectIdsInitialized = ref(false)
|
||||
const isServerInstance = ref(false)
|
||||
|
||||
const breadcrumbLabel = ref(
|
||||
displayedBrowseRoute.value.query.from === 'worlds'
|
||||
? 'Discover servers'
|
||||
: 'Discover content',
|
||||
)
|
||||
const instanceBreadcrumb = route.query.i
|
||||
? useBreadcrumb({
|
||||
slot: 'instance',
|
||||
id: () => `instance:${String(displayedBrowseRoute.value.query.i ?? '')}`,
|
||||
label: () => instance.value?.name ?? formatMessage(commonMessages.loadingLabel),
|
||||
to: () =>
|
||||
`/instance/${encodeURIComponent(String(displayedBrowseRoute.value.query.i ?? ''))}`,
|
||||
})
|
||||
: undefined
|
||||
const instanceSectionBreadcrumb = instanceBreadcrumb
|
||||
? useBreadcrumb(
|
||||
{
|
||||
slot: 'instance-section',
|
||||
id: () =>
|
||||
`instance-section:${String(displayedBrowseRoute.value.query.i ?? '')}:${
|
||||
displayedBrowseRoute.value.query.from === 'worlds' ? 'worlds' : 'content'
|
||||
}`,
|
||||
label: () =>
|
||||
displayedBrowseRoute.value.query.from === 'worlds' ? 'Worlds' : 'Content',
|
||||
to: () => {
|
||||
const instancePath = `/instance/${encodeURIComponent(
|
||||
String(displayedBrowseRoute.value.query.i ?? ''),
|
||||
)}`
|
||||
return displayedBrowseRoute.value.query.from === 'worlds'
|
||||
? `${instancePath}/worlds`
|
||||
: instancePath
|
||||
},
|
||||
},
|
||||
{ parent: instanceBreadcrumb },
|
||||
)
|
||||
: undefined
|
||||
const serverBreadcrumbTo = ref(serverBackUrl.value)
|
||||
watch(serverBackUrl, (value) => {
|
||||
if (route.path.startsWith('/browse/')) {
|
||||
serverBreadcrumbTo.value = value
|
||||
}
|
||||
})
|
||||
const serverBreadcrumb = !instanceBreadcrumb && serverIdQuery.value
|
||||
? useBreadcrumb({
|
||||
slot: 'server',
|
||||
id: () => `server:${String(displayedBrowseRoute.value.query.sid ?? '')}`,
|
||||
label: () =>
|
||||
serverContextServerData.value?.name ?? formatMessage(commonMessages.loadingLabel),
|
||||
to: serverBreadcrumbTo,
|
||||
})
|
||||
: undefined
|
||||
const breadcrumbParent = instanceSectionBreadcrumb ?? serverBreadcrumb
|
||||
const breadcrumbDefinition = {
|
||||
slot: 'browse',
|
||||
id: () =>
|
||||
`browse:${String(displayedBrowseRoute.value.params.projectType ?? '')}:${String(
|
||||
displayedBrowseRoute.value.query.i ?? '',
|
||||
)}:${String(displayedBrowseRoute.value.query.sid ?? '')}:${String(
|
||||
displayedBrowseRoute.value.query.from ?? '',
|
||||
)}`,
|
||||
label: breadcrumbLabel,
|
||||
to: () => displayedBrowseRoute.value.fullPath,
|
||||
}
|
||||
const browseBreadcrumb = breadcrumbParent
|
||||
? useBreadcrumb(breadcrumbDefinition, { parent: breadcrumbParent })
|
||||
: useRootBreadcrumb(breadcrumbDefinition)
|
||||
|
||||
debugLog('fetching tags (categories, loaders, gameVersions)')
|
||||
const [categories, loaders, availableGameVersions] = await Promise.all([
|
||||
get_categories()
|
||||
.catch(handleError)
|
||||
.then(ref<Labrinth.Tags.v2.Category[]>),
|
||||
get_loaders()
|
||||
.catch(handleError)
|
||||
.then(ref<Labrinth.Tags.v2.Loader[]>),
|
||||
get_game_versions()
|
||||
.catch(handleError)
|
||||
.then(ref<Labrinth.Tags.v2.GameVersion[]>),
|
||||
])
|
||||
|
||||
const tags: Ref<Tags> = computed(() => ({
|
||||
gameVersions: availableGameVersions.value ?? [],
|
||||
loaders: loaders.value ?? [],
|
||||
categories: categories.value ?? [],
|
||||
}))
|
||||
|
||||
if (isFromWorlds.value && route.params.projectType !== 'server') {
|
||||
router.replace({
|
||||
path: '/browse/server',
|
||||
@@ -448,28 +528,16 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const browseTitle = computed(() =>
|
||||
formatMessage(isFromWorlds.value ? messages.discoverServers : messages.discoverContent),
|
||||
)
|
||||
breadcrumbs.setName('BrowseTitle', browseTitle.value)
|
||||
if (instance.value) {
|
||||
const instanceLink = `/instance/${encodeURIComponent(instance.value.id)}`
|
||||
breadcrumbs.setContext({
|
||||
name: instance.value.name,
|
||||
link: isFromWorlds.value ? `${instanceLink}/worlds` : instanceLink,
|
||||
})
|
||||
} else {
|
||||
breadcrumbs.setContext(null)
|
||||
}
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
breadcrumbs.setContext({
|
||||
name: browseTitle.value,
|
||||
link: `/browse/${projectType.value}`,
|
||||
query: route.query,
|
||||
})
|
||||
})
|
||||
watch(
|
||||
browseTitle,
|
||||
(value) => {
|
||||
breadcrumbLabel.value = value
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const projectType = ref<ProjectType>(route.params.projectType as ProjectType)
|
||||
|
||||
@@ -484,8 +552,7 @@ function resetInstanceContext() {
|
||||
hiddenInstanceProjectIds.value = new Set()
|
||||
hiddenInstanceProjectIdsInitialized.value = false
|
||||
isServerInstance.value = false
|
||||
breadcrumbs.setName('BrowseTitle', formatMessage(messages.discoverContent))
|
||||
breadcrumbs.setContext(null)
|
||||
browseBreadcrumb.reset()
|
||||
}
|
||||
|
||||
watch(
|
||||
|
||||
@@ -3,7 +3,6 @@ import { injectNotificationManager } from '@modrinth/ui'
|
||||
import type { SearchResult } from '@modrinth/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import RowDisplay from '@/components/RowDisplay.vue'
|
||||
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
|
||||
@@ -11,13 +10,16 @@ import { get_search_results } from '@/helpers/cache.js'
|
||||
import { instance_listener } from '@/helpers/events'
|
||||
import { list } from '@/helpers/instance'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const route = useRoute()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
breadcrumbs.setRootContext({ name: 'Home', link: route.path })
|
||||
useRootBreadcrumb({
|
||||
slot: 'root',
|
||||
id: 'home',
|
||||
label: 'Home',
|
||||
to: '/',
|
||||
})
|
||||
|
||||
const instances = ref<GameInstance[]>([])
|
||||
|
||||
|
||||
@@ -4,12 +4,21 @@ import { injectModrinthClient, ServersManagePageIndex } from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
|
||||
|
||||
import { config } from '../config'
|
||||
|
||||
const stripePublishableKey = (config.stripePublishableKey as string) || ''
|
||||
|
||||
const client = injectModrinthClient()
|
||||
|
||||
useRootBreadcrumb({
|
||||
slot: 'root',
|
||||
id: 'servers',
|
||||
label: 'Servers',
|
||||
to: '/hosting/manage/',
|
||||
})
|
||||
|
||||
const { data: products } = useQuery({
|
||||
queryKey: ['billing', 'products'],
|
||||
queryFn: () => client.labrinth.billing_internal.getProducts(),
|
||||
|
||||
@@ -56,9 +56,17 @@ import {
|
||||
set_custom_skin_order,
|
||||
} from '@/helpers/skins.ts'
|
||||
import { hasPride26Badge } from '@/helpers/user-campaigns.ts'
|
||||
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
useRootBreadcrumb({
|
||||
slot: 'root',
|
||||
id: 'skins',
|
||||
label: 'Skin selector',
|
||||
to: '/skins',
|
||||
})
|
||||
|
||||
type UnlistenFn = () => void
|
||||
type VirtualSkinSectionListExpose = {
|
||||
getAddSkinButtonElement: () => HTMLElement | null | undefined
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<script setup lang="ts">
|
||||
import { provideUserProfile, UserProfilePageLayout } from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
|
||||
|
||||
import {
|
||||
@@ -24,11 +24,10 @@ import {
|
||||
get_user_projects,
|
||||
patch_user,
|
||||
} from '@/helpers/users'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
|
||||
|
||||
const route = useRoute()
|
||||
const queryClient = useQueryClient()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const userProfile = provideUserProfile({
|
||||
getUser: get_user_profile,
|
||||
getProjects: get_user_projects,
|
||||
@@ -46,17 +45,43 @@ const projectType = computed(() => {
|
||||
return Array.isArray(value) ? value[0] : value
|
||||
})
|
||||
|
||||
const { data: user } = useQuery({
|
||||
queryKey: computed(() => ['user', userId.value]),
|
||||
queryFn: () => userProfile.getUser(userId.value),
|
||||
enabled: false,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const breadcrumbUserId = ref(userId.value)
|
||||
const breadcrumbLabel = ref(userId.value)
|
||||
const breadcrumbTo = ref(route.fullPath)
|
||||
watch(
|
||||
[userId, user, () => route.fullPath],
|
||||
([currentUserId, currentUser, currentPath]) => {
|
||||
if (route.name !== 'User') return
|
||||
breadcrumbUserId.value = currentUserId
|
||||
breadcrumbLabel.value = currentUser?.username ?? currentUserId
|
||||
breadcrumbTo.value = currentPath
|
||||
},
|
||||
{ immediate: true, flush: 'sync' },
|
||||
)
|
||||
|
||||
useRootBreadcrumb({
|
||||
slot: 'root',
|
||||
id: () => `user:${breadcrumbUserId.value}`,
|
||||
label: breadcrumbLabel,
|
||||
to: breadcrumbTo,
|
||||
})
|
||||
|
||||
async function ensureUserProfileData(id: string): Promise<void> {
|
||||
if (!id) return
|
||||
|
||||
let breadcrumbName = id
|
||||
try {
|
||||
const user = await queryClient.ensureQueryData({
|
||||
await queryClient.ensureQueryData({
|
||||
queryKey: ['user', id],
|
||||
queryFn: () => userProfile.getUser(id),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
breadcrumbName = user.username
|
||||
} catch {
|
||||
// Let the mounted layout's useQuery surface errors; do not fail route setup.
|
||||
}
|
||||
@@ -78,8 +103,6 @@ async function ensureUserProfileData(id: string): Promise<void> {
|
||||
staleTime: 30_000,
|
||||
}),
|
||||
])
|
||||
|
||||
breadcrumbs.setName('User', breadcrumbName)
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate(async (to) => {
|
||||
@@ -88,21 +111,5 @@ onBeforeRouteUpdate(async (to) => {
|
||||
await ensureUserProfileData(id)
|
||||
})
|
||||
|
||||
breadcrumbs.setName('User', userId.value)
|
||||
await ensureUserProfileData(userId.value)
|
||||
|
||||
const { data: user } = useQuery({
|
||||
queryKey: computed(() => ['user', userId.value]),
|
||||
queryFn: () => userProfile.getUser(userId.value),
|
||||
enabled: false,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
watch(
|
||||
[userId, user],
|
||||
([currentUserId, value]) => {
|
||||
breadcrumbs.setName('User', value?.username ?? currentUserId)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
|
||||
|
||||
useRootBreadcrumb({
|
||||
slot: 'root',
|
||||
id: 'worlds',
|
||||
label: 'Worlds',
|
||||
to: '/worlds',
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="p-6 flex flex-col gap-2">Worlds</div>
|
||||
</template>
|
||||
|
||||
@@ -48,15 +48,24 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import { injectAuth, injectModrinthClient, ServersManageRootLayout } from '@modrinth/ui'
|
||||
import {
|
||||
commonMessages,
|
||||
injectAuth,
|
||||
injectModrinthClient,
|
||||
ServersManageRootLayout,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { get_user } from '@/helpers/cache'
|
||||
import { get as getCreds } from '@/helpers/mr_auth'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import {
|
||||
provideBreadcrumbParent,
|
||||
useBreadcrumb,
|
||||
} from '@/providers/breadcrumbs'
|
||||
import { useTheming } from '@/store/theme'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -65,15 +74,49 @@ const auth = injectAuth()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const themeStore = useTheming()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const isContainedServerRoute = computed(() => route.name === 'ServerManageOverview')
|
||||
|
||||
const serverId = computed(() => {
|
||||
const rawId = route.params.id
|
||||
return Array.isArray(rawId) ? rawId[0] : (rawId ?? '')
|
||||
return Array.isArray(rawId) ? (rawId[0] ?? '') : (rawId ?? '')
|
||||
})
|
||||
|
||||
const { data: serverData } = useQuery({
|
||||
queryKey: computed(() => ['servers', 'detail', serverId.value]),
|
||||
queryFn: () => null as unknown as Archon.Servers.v0.Server,
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
const breadcrumbServerId = ref(serverId.value)
|
||||
const breadcrumbLabel = ref(formatMessage(commonMessages.loadingLabel))
|
||||
watch(
|
||||
serverId,
|
||||
(value) => {
|
||||
if (!route.path.startsWith('/hosting/manage/') || route.name === 'Servers') return
|
||||
breadcrumbServerId.value = value
|
||||
breadcrumbLabel.value = formatMessage(commonMessages.loadingLabel)
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
)
|
||||
watch(
|
||||
serverData,
|
||||
(server) => {
|
||||
if (!route.path.startsWith('/hosting/manage/') || !server?.name) return
|
||||
breadcrumbLabel.value = server.name
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const serverBreadcrumb = useBreadcrumb({
|
||||
slot: 'server',
|
||||
id: () => `server:${breadcrumbServerId.value}`,
|
||||
label: breadcrumbLabel,
|
||||
to: () => `/hosting/manage/${encodeURIComponent(breadcrumbServerId.value)}`,
|
||||
})
|
||||
provideBreadcrumbParent(serverBreadcrumb)
|
||||
|
||||
if (serverId.value) {
|
||||
try {
|
||||
await queryClient.ensureQueryData({
|
||||
@@ -86,26 +129,6 @@ if (serverId.value) {
|
||||
}
|
||||
}
|
||||
|
||||
const { data: serverData } = useQuery({
|
||||
queryKey: computed(() => ['servers', 'detail', serverId.value]),
|
||||
queryFn: () => null as unknown as Archon.Servers.v0.Server,
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
watch(
|
||||
serverData,
|
||||
(server) => {
|
||||
if (server?.name) {
|
||||
breadcrumbs.setName('Server', server.name)
|
||||
breadcrumbs.setContext({
|
||||
name: server.name,
|
||||
link: `/hosting/manage/${serverId.value}/content`,
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => auth.user.value,
|
||||
(user, previousUser) => {
|
||||
|
||||
@@ -138,7 +138,14 @@ import {
|
||||
UserPlusIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { injectAuth, injectNotificationManager, NavTabs, useLoadingBarToken } from '@modrinth/ui'
|
||||
import {
|
||||
commonMessages,
|
||||
injectAuth,
|
||||
injectNotificationManager,
|
||||
NavTabs,
|
||||
useLoadingBarToken,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -187,7 +194,11 @@ import { createInstanceShortcut, showInstanceInFolder } from '@/helpers/utils.js
|
||||
import { refreshWorlds, type ServerStatus } from '@/helpers/worlds'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { useBreadcrumbs, useTheming } from '@/store/state'
|
||||
import {
|
||||
provideBreadcrumbParent,
|
||||
useBreadcrumb,
|
||||
} from '@/providers/breadcrumbs'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
import { provideSharedInstanceState, useSharedInstanceState } from './use-shared-instance-state'
|
||||
|
||||
@@ -198,10 +209,10 @@ const { playServerProject } = injectServerInstall()
|
||||
const auth = injectAuth()
|
||||
const queryClient = useQueryClient()
|
||||
const route = useRoute()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const router = useRouter()
|
||||
const displayedInstanceRoute = shallowRef(router.currentRoute.value)
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const themeStore = useTheming()
|
||||
const showInstancePlayTime = computed(() => themeStore.getFeatureFlag('show_instance_play_time'))
|
||||
const contentSubpageRouteNames = new Set(['Mods', 'ModsFilter'])
|
||||
@@ -215,6 +226,45 @@ window.addEventListener('online', () => {
|
||||
})
|
||||
|
||||
const instance = ref<GameInstance>()
|
||||
const instanceBreadcrumb = useBreadcrumb({
|
||||
slot: 'instance',
|
||||
id: () => `instance:${String(displayedInstanceRoute.value.params.id ?? '')}`,
|
||||
label: () =>
|
||||
instance.value?.id === displayedInstanceRoute.value.params.id
|
||||
? instance.value.name
|
||||
: formatMessage(commonMessages.loadingLabel),
|
||||
to: () =>
|
||||
`/instance/${encodeURIComponent(String(displayedInstanceRoute.value.params.id ?? ''))}`,
|
||||
})
|
||||
provideBreadcrumbParent(instanceBreadcrumb)
|
||||
|
||||
const instanceSection = computed(() => {
|
||||
switch (displayedInstanceRoute.value.name) {
|
||||
case 'InstanceWorlds':
|
||||
return { id: 'worlds', label: 'Worlds' }
|
||||
case 'InstanceShare':
|
||||
return { id: 'share', label: 'Share' }
|
||||
case 'Files':
|
||||
return { id: 'files', label: 'Files' }
|
||||
case 'Logs':
|
||||
return { id: 'logs', label: 'Logs' }
|
||||
default:
|
||||
return { id: 'content', label: 'Content' }
|
||||
}
|
||||
})
|
||||
useBreadcrumb(
|
||||
{
|
||||
slot: 'instance-section',
|
||||
id: () =>
|
||||
`instance-section:${String(displayedInstanceRoute.value.params.id ?? '')}:${
|
||||
instanceSection.value.id
|
||||
}`,
|
||||
label: () => instanceSection.value.label,
|
||||
to: () => displayedInstanceRoute.value.fullPath,
|
||||
},
|
||||
{ parent: instanceBreadcrumb },
|
||||
)
|
||||
|
||||
const preloadedContent = ref<InstanceContentData | null>(null)
|
||||
const playing = ref(false)
|
||||
const loading = ref(false)
|
||||
@@ -503,20 +553,6 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
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 launchInstance = async (context: string) => {
|
||||
|
||||
@@ -7,14 +7,18 @@ import { useRoute } from 'vue-router'
|
||||
import { NewInstanceImage } from '@/assets/icons'
|
||||
import { instance_listener } from '@/helpers/events.js'
|
||||
import { list } from '@/helpers/instance'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs.js'
|
||||
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const showCreationModal = inject('showCreationModal')
|
||||
const route = useRoute()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
breadcrumbs.setRootContext({ name: 'Library', link: route.path })
|
||||
useRootBreadcrumb({
|
||||
slot: 'root',
|
||||
id: 'library',
|
||||
label: 'Library',
|
||||
to: '/library',
|
||||
})
|
||||
|
||||
const instances = shallowRef(await list().catch(handleError))
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { useBreadcrumb } from '@/providers/breadcrumbs'
|
||||
|
||||
const MC_SERVER_BANNER_NAME = '__mc_server_banner__'
|
||||
|
||||
@@ -111,6 +112,16 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
useBreadcrumb({
|
||||
slot: 'project-section',
|
||||
id: () => `project-gallery:${props.project?.id ?? ''}`,
|
||||
label: 'Gallery',
|
||||
to: () => ({
|
||||
name: 'Gallery',
|
||||
params: { id: props.project?.id ?? '' },
|
||||
}),
|
||||
})
|
||||
|
||||
const filteredGallery = computed(
|
||||
() => props.project.gallery?.filter((img) => img.title !== MC_SERVER_BANNER_NAME) ?? [],
|
||||
)
|
||||
|
||||
@@ -298,7 +298,10 @@ import { getServerAddress } from '@/helpers/worlds'
|
||||
import { injectContentInstall } from '@/providers/content-install'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
import { createServerInstallContent } from '@/providers/setup/server-install-content'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import {
|
||||
provideBreadcrumbParent,
|
||||
useBreadcrumb,
|
||||
} from '@/providers/breadcrumbs'
|
||||
import { useTheming } from '@/store/state.js'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
@@ -307,8 +310,17 @@ const { handleError } = injectNotificationManager()
|
||||
const { install: installVersion } = injectContentInstall()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const displayedProjectRoute = shallowRef(router.currentRoute.value)
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
(nextRoute) => {
|
||||
if (nextRoute.path.startsWith('/project/')) {
|
||||
displayedProjectRoute.value = nextRoute
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
const queryClient = useQueryClient()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const themeStore = useTheming()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -331,6 +343,21 @@ const { installingServerProjects, playServerProject, showAddServerToInstanceModa
|
||||
injectServerInstall()
|
||||
const installing = ref(false)
|
||||
const data = shallowRef(null)
|
||||
const projectBreadcrumbLabel = ref(
|
||||
String(route.params.id ?? formatMessage(commonMessages.loadingLabel)),
|
||||
)
|
||||
const projectBreadcrumb = useBreadcrumb({
|
||||
slot: 'project',
|
||||
id: () => `project:${String(displayedProjectRoute.value.params.id ?? '')}`,
|
||||
label: projectBreadcrumbLabel,
|
||||
to: () => ({
|
||||
name: 'Description',
|
||||
params: { id: displayedProjectRoute.value.params.id },
|
||||
query: displayedProjectRoute.value.query,
|
||||
}),
|
||||
})
|
||||
provideBreadcrumbParent(projectBreadcrumb)
|
||||
|
||||
const versions = shallowRef([])
|
||||
const members = shallowRef([])
|
||||
const categories = shallowRef([])
|
||||
@@ -607,6 +634,9 @@ function reportProject() {
|
||||
}
|
||||
|
||||
async function fetchProjectData() {
|
||||
projectBreadcrumbLabel.value = String(
|
||||
route.params.id ?? formatMessage(commonMessages.loadingLabel),
|
||||
)
|
||||
const [project, projectV3Result] = await Promise.all([
|
||||
get_project(route.params.id, 'must_revalidate').catch(handleError),
|
||||
get_project_v3(route.params.id, 'must_revalidate').catch(handleError),
|
||||
@@ -619,6 +649,7 @@ async function fetchProjectData() {
|
||||
}
|
||||
|
||||
data.value = project
|
||||
projectBreadcrumbLabel.value = project.title
|
||||
;[versions.value, members.value, categories.value, instance.value, instanceProjects.value] =
|
||||
await Promise.all([
|
||||
get_version_many(project.versions, 'must_revalidate').catch(handleError),
|
||||
@@ -647,8 +678,6 @@ async function fetchProjectData() {
|
||||
isServerProject.value = projectV3.value?.minecraft_server != null
|
||||
serverStatusOnline.value = !!projectV3.value?.minecraft_java_server?.ping?.data
|
||||
|
||||
breadcrumbs.setName('Project', data.value.title)
|
||||
|
||||
fetchDeferredServerData(project)
|
||||
}
|
||||
|
||||
|
||||
@@ -95,12 +95,12 @@ import {
|
||||
useVIntl,
|
||||
VersionPage,
|
||||
} from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons'
|
||||
import { get_project_many, get_version_many } from '@/helpers/cache.js'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { useBreadcrumb } from '@/providers/breadcrumbs'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -119,8 +119,18 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const displayedVersionRoute = shallowRef(router.currentRoute.value)
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
(nextRoute) => {
|
||||
if (nextRoute.name === 'Version') {
|
||||
displayedVersionRoute.value = nextRoute
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const props = defineProps<{
|
||||
project: Labrinth.Projects.v2.Project
|
||||
@@ -133,9 +143,33 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const version = ref(props.versions.find((version) => version.id === route.params.version))
|
||||
if (version.value) {
|
||||
breadcrumbs.setName('Version', version.value.name)
|
||||
}
|
||||
const versionsBreadcrumb = useBreadcrumb({
|
||||
slot: 'project-section',
|
||||
id: () => `project-versions:${props.project.id}`,
|
||||
label: 'Versions',
|
||||
to: () => ({
|
||||
name: 'Versions',
|
||||
params: { id: props.project.id },
|
||||
query: displayedVersionRoute.value.query,
|
||||
}),
|
||||
})
|
||||
useBreadcrumb(
|
||||
{
|
||||
slot: 'project-version',
|
||||
id: () =>
|
||||
`version:${props.project.id}:${String(
|
||||
displayedVersionRoute.value.params.version ?? '',
|
||||
)}`,
|
||||
label: () =>
|
||||
version.value?.name ??
|
||||
String(
|
||||
displayedVersionRoute.value.params.version ??
|
||||
formatMessage(commonMessages.loadingLabel),
|
||||
),
|
||||
to: () => displayedVersionRoute.value.fullPath,
|
||||
},
|
||||
{ parent: versionsBreadcrumb },
|
||||
)
|
||||
|
||||
const enrichment = ref<Labrinth.Projects.v2.DependencyInfo | undefined>(undefined)
|
||||
const enrichmentLoading = ref(false)
|
||||
@@ -203,13 +237,10 @@ async function refreshEnrichment() {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.versions,
|
||||
[() => props.versions, () => route.params.version],
|
||||
async () => {
|
||||
if (route.params.version) {
|
||||
version.value = props.versions.find((v) => v.id === route.params.version)
|
||||
if (version.value) {
|
||||
breadcrumbs.setName('Version', version.value.name)
|
||||
}
|
||||
await refreshEnrichment()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -82,11 +82,12 @@ import {
|
||||
ProjectPageVersions,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
||||
import { useBreadcrumb } from '@/providers/breadcrumbs'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -99,7 +100,7 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
@@ -132,6 +133,28 @@ defineProps({
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const displayedVersionsRoute = shallowRef(router.currentRoute.value)
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
(nextRoute) => {
|
||||
if (nextRoute.name === 'Versions') {
|
||||
displayedVersionsRoute.value = nextRoute
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
useBreadcrumb({
|
||||
slot: 'project-section',
|
||||
id: () => `project-versions:${props.project?.id ?? ''}`,
|
||||
label: 'Versions',
|
||||
to: () => ({
|
||||
name: 'Versions',
|
||||
params: { id: props.project?.id ?? route.params.id },
|
||||
query: displayedVersionsRoute.value.query,
|
||||
}),
|
||||
})
|
||||
|
||||
function buildProjectHref(path) {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { createContext } from '@modrinth/ui'
|
||||
import {
|
||||
computed,
|
||||
type MaybeRefOrGetter,
|
||||
type ComputedRef,
|
||||
shallowRef,
|
||||
toValue,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
export interface BreadcrumbDefinition {
|
||||
slot: string
|
||||
id: MaybeRefOrGetter<string>
|
||||
label: MaybeRefOrGetter<string>
|
||||
to?: MaybeRefOrGetter<RouteLocationRaw | undefined>
|
||||
}
|
||||
|
||||
export interface ResolvedBreadcrumb {
|
||||
slot: string
|
||||
id: string
|
||||
label: string
|
||||
to?: RouteLocationRaw
|
||||
}
|
||||
|
||||
export interface BreadcrumbHandle {
|
||||
readonly slot: string
|
||||
activate: () => void
|
||||
reset: () => void
|
||||
pop: () => void
|
||||
}
|
||||
|
||||
export interface BreadcrumbManager {
|
||||
readonly entries: ComputedRef<ResolvedBreadcrumb[]>
|
||||
reset: (definition: BreadcrumbDefinition) => BreadcrumbHandle
|
||||
push: (
|
||||
definition: BreadcrumbDefinition,
|
||||
options?: { parent?: BreadcrumbHandle },
|
||||
) => BreadcrumbHandle
|
||||
find: (slot: string) => BreadcrumbHandle | undefined
|
||||
}
|
||||
|
||||
interface InternalBreadcrumb {
|
||||
token: symbol
|
||||
definition: BreadcrumbDefinition
|
||||
parentToken?: symbol
|
||||
parentSlot?: string
|
||||
root: boolean
|
||||
handle: BreadcrumbHandle
|
||||
}
|
||||
|
||||
const [injectBreadcrumbManager, provideBreadcrumbManager] = createContext<BreadcrumbManager>(
|
||||
'root',
|
||||
'breadcrumbManager',
|
||||
)
|
||||
const [injectBreadcrumbParent, provideBreadcrumbParent] = createContext<BreadcrumbHandle>(
|
||||
'BreadcrumbParent',
|
||||
'breadcrumbParent',
|
||||
)
|
||||
|
||||
export { injectBreadcrumbManager, provideBreadcrumbManager, provideBreadcrumbParent }
|
||||
|
||||
export function createBreadcrumbManager(): BreadcrumbManager {
|
||||
const stack = shallowRef<InternalBreadcrumb[]>([])
|
||||
|
||||
function findIndex(entry: InternalBreadcrumb): number {
|
||||
const tokenIndex = stack.value.findIndex((candidate) => candidate.token === entry.token)
|
||||
if (tokenIndex !== -1) return tokenIndex
|
||||
return stack.value.findIndex((candidate) => candidate.definition.slot === entry.definition.slot)
|
||||
}
|
||||
|
||||
function findParentIndex(entry: InternalBreadcrumb): number {
|
||||
if (entry.parentToken) {
|
||||
const tokenIndex = stack.value.findIndex(
|
||||
(candidate) => candidate.token === entry.parentToken,
|
||||
)
|
||||
if (tokenIndex !== -1) return tokenIndex
|
||||
}
|
||||
if (entry.parentSlot) {
|
||||
return stack.value.findIndex(
|
||||
(candidate) => candidate.definition.slot === entry.parentSlot,
|
||||
)
|
||||
}
|
||||
return stack.value.length - 1
|
||||
}
|
||||
|
||||
function activate(entry: InternalBreadcrumb) {
|
||||
if (entry.root) {
|
||||
stack.value = [entry]
|
||||
return
|
||||
}
|
||||
|
||||
const existingIndex = findIndex(entry)
|
||||
if (existingIndex !== -1) {
|
||||
stack.value = [...stack.value.slice(0, existingIndex), entry]
|
||||
return
|
||||
}
|
||||
|
||||
const parentIndex = findParentIndex(entry)
|
||||
stack.value = [...stack.value.slice(0, parentIndex + 1), entry]
|
||||
}
|
||||
|
||||
function resetTo(entry: InternalBreadcrumb) {
|
||||
stack.value = [entry]
|
||||
}
|
||||
|
||||
function pop(entry: InternalBreadcrumb) {
|
||||
const index = stack.value.findIndex((candidate) => candidate.token === entry.token)
|
||||
if (index !== -1) {
|
||||
stack.value = stack.value.slice(0, index)
|
||||
}
|
||||
}
|
||||
|
||||
function createHandle(
|
||||
definition: BreadcrumbDefinition,
|
||||
options: { parent?: BreadcrumbHandle; root: boolean },
|
||||
): BreadcrumbHandle {
|
||||
const entry = {} as InternalBreadcrumb
|
||||
const handle: BreadcrumbHandle = {
|
||||
slot: definition.slot,
|
||||
activate: () => activate(entry),
|
||||
reset: () => resetTo(entry),
|
||||
pop: () => pop(entry),
|
||||
}
|
||||
|
||||
Object.assign(entry, {
|
||||
token: Symbol(definition.slot),
|
||||
definition,
|
||||
parentToken: options.parent
|
||||
? stack.value.find((candidate) => candidate.handle === options.parent)?.token
|
||||
: undefined,
|
||||
parentSlot: options.parent?.slot,
|
||||
root: options.root,
|
||||
handle,
|
||||
})
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
function reset(definition: BreadcrumbDefinition): BreadcrumbHandle {
|
||||
const handle = createHandle(definition, { root: true })
|
||||
handle.reset()
|
||||
return handle
|
||||
}
|
||||
|
||||
function push(
|
||||
definition: BreadcrumbDefinition,
|
||||
options: { parent?: BreadcrumbHandle } = {},
|
||||
): BreadcrumbHandle {
|
||||
const handle = createHandle(definition, { ...options, root: false })
|
||||
handle.activate()
|
||||
return handle
|
||||
}
|
||||
|
||||
const entries = computed<ResolvedBreadcrumb[]>(() =>
|
||||
stack.value.map(({ definition }) => ({
|
||||
slot: definition.slot,
|
||||
id: toValue(definition.id),
|
||||
label: toValue(definition.label),
|
||||
to: definition.to === undefined ? undefined : toValue(definition.to),
|
||||
})),
|
||||
)
|
||||
|
||||
return {
|
||||
entries,
|
||||
reset,
|
||||
push,
|
||||
find: (slot) =>
|
||||
stack.value.find((entry) => entry.definition.slot === slot)?.handle,
|
||||
}
|
||||
}
|
||||
|
||||
function watchBreadcrumbIdentity(
|
||||
definition: BreadcrumbDefinition,
|
||||
handle: BreadcrumbHandle,
|
||||
) {
|
||||
watch(
|
||||
() => toValue(definition.id),
|
||||
() => handle.activate(),
|
||||
{ flush: 'sync' },
|
||||
)
|
||||
}
|
||||
|
||||
export function useRootBreadcrumb(definition: BreadcrumbDefinition): BreadcrumbHandle {
|
||||
const manager = injectBreadcrumbManager()
|
||||
const handle = manager.reset(definition)
|
||||
watchBreadcrumbIdentity(definition, handle)
|
||||
return handle
|
||||
}
|
||||
|
||||
export function useBreadcrumb(
|
||||
definition: BreadcrumbDefinition,
|
||||
options: { parent?: BreadcrumbHandle } = {},
|
||||
): BreadcrumbHandle {
|
||||
const manager = injectBreadcrumbManager()
|
||||
const parent = options.parent ?? injectBreadcrumbParent(null) ?? undefined
|
||||
const handle = manager.push(definition, { parent })
|
||||
watchBreadcrumbIdentity(definition, handle)
|
||||
return handle
|
||||
}
|
||||
@@ -16,25 +16,16 @@ export default new createRouter({
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Pages.Index,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'Home' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/worlds',
|
||||
name: 'Worlds',
|
||||
component: Pages.Worlds,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'Worlds' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/hosting/manage/',
|
||||
name: 'Servers',
|
||||
component: Pages.Servers,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'Servers' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/hosting/manage/:id',
|
||||
@@ -45,41 +36,26 @@ export default new createRouter({
|
||||
path: '',
|
||||
name: 'ServerManageOverview',
|
||||
component: Hosting.Overview,
|
||||
meta: {
|
||||
breadcrumb: [{ name: '?Server' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'content',
|
||||
name: 'ServerManageContent',
|
||||
component: Hosting.Content,
|
||||
meta: {
|
||||
breadcrumb: [{ name: '?Server' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'files',
|
||||
name: 'ServerManageFiles',
|
||||
component: Hosting.Files,
|
||||
meta: {
|
||||
breadcrumb: [{ name: '?Server' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'backups',
|
||||
name: 'ServerManageBackups',
|
||||
component: Hosting.Backups,
|
||||
meta: {
|
||||
breadcrumb: [{ name: '?Server' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'access',
|
||||
name: 'ServerManageAccess',
|
||||
component: Hosting.Access,
|
||||
meta: {
|
||||
breadcrumb: [{ name: '?Server' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -87,34 +63,21 @@ export default new createRouter({
|
||||
path: '/browse/:projectType',
|
||||
name: 'Discover content',
|
||||
component: Pages.Browse,
|
||||
meta: {
|
||||
useContext: true,
|
||||
breadcrumb: [{ name: '?BrowseTitle' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/skins',
|
||||
name: 'Skin selector',
|
||||
component: Pages.Skins,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'Skin selector' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/user/:user/:projectType?',
|
||||
name: 'User',
|
||||
component: Pages.User,
|
||||
meta: {
|
||||
breadcrumb: [{ name: '?User' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/library',
|
||||
name: 'Library',
|
||||
component: Library.Index,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'Library' }],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
@@ -160,42 +123,22 @@ export default new createRouter({
|
||||
path: '',
|
||||
name: 'Description',
|
||||
component: Project.Description,
|
||||
meta: {
|
||||
useContext: true,
|
||||
breadcrumb: [{ name: '?Project' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'versions',
|
||||
name: 'Versions',
|
||||
component: Project.Versions,
|
||||
meta: {
|
||||
useContext: true,
|
||||
breadcrumb: [{ name: '?Project', link: '/project/{id}/' }, { name: 'Versions' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'version/:version',
|
||||
name: 'Version',
|
||||
component: Project.Version,
|
||||
props: true,
|
||||
meta: {
|
||||
useContext: true,
|
||||
breadcrumb: [
|
||||
{ name: '?Project', link: '/project/{id}/' },
|
||||
{ name: 'Versions', link: '/project/{id}/versions' },
|
||||
{ name: '?Version' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'gallery',
|
||||
name: 'Gallery',
|
||||
component: Project.Gallery,
|
||||
meta: {
|
||||
useContext: true,
|
||||
breadcrumb: [{ name: '?Project', link: '/project/{id}/' }, { name: 'Gallery' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -205,59 +148,30 @@ export default new createRouter({
|
||||
component: Instance.Index,
|
||||
props: true,
|
||||
children: [
|
||||
// {
|
||||
// path: '',
|
||||
// name: 'Overview',
|
||||
// component: Instance.Overview,
|
||||
// meta: {
|
||||
// useRootContext: true,
|
||||
// breadcrumb: [{ name: '?Instance' }],
|
||||
// },
|
||||
// },
|
||||
{
|
||||
path: 'worlds',
|
||||
name: 'InstanceWorlds',
|
||||
component: Instance.Worlds,
|
||||
meta: {
|
||||
useRootContext: true,
|
||||
breadcrumb: [{ name: '?Instance', link: '/instance/{id}/' }, { name: 'Worlds' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'share',
|
||||
name: 'InstanceShare',
|
||||
component: Instance.Share,
|
||||
meta: {
|
||||
useRootContext: true,
|
||||
breadcrumb: [{ name: '?Instance', link: '/instance/{id}/' }, { name: 'Share' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
name: 'Mods',
|
||||
component: Instance.Mods,
|
||||
meta: {
|
||||
useRootContext: true,
|
||||
breadcrumb: [{ name: '?Instance', link: '/instance/{id}/' }, { name: 'Content' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'projects/:type',
|
||||
name: 'ModsFilter',
|
||||
component: Instance.Mods,
|
||||
meta: {
|
||||
useRootContext: true,
|
||||
breadcrumb: [{ name: '?Instance', link: '/instance/{id}/' }, { name: 'Content' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'files',
|
||||
name: 'Files',
|
||||
component: Instance.Files,
|
||||
meta: {
|
||||
useRootContext: true,
|
||||
breadcrumb: [{ name: '?Instance', link: '/instance/{id}/' }, { name: 'Files' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
@@ -265,8 +179,6 @@ export default new createRouter({
|
||||
component: Instance.Logs,
|
||||
meta: {
|
||||
renderMode: 'fixed',
|
||||
useRootContext: true,
|
||||
breadcrumb: [{ name: '?Instance', link: '/instance/{id}/' }, { name: 'Logs' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useBreadcrumbs = defineStore('breadcrumbsStore', {
|
||||
state: () => ({
|
||||
names: new Map(),
|
||||
context: null,
|
||||
rootContext: null,
|
||||
}),
|
||||
actions: {
|
||||
getName(route) {
|
||||
return this.names.get(route) ?? ''
|
||||
},
|
||||
setName(route, title) {
|
||||
this.names.set(route, title)
|
||||
},
|
||||
// resets breadcrumbs to only included ones as to not have stale breadcrumbs
|
||||
resetToNames(breadcrumbs) {
|
||||
if (!breadcrumbs) return
|
||||
// names is an array of every breadcrumb.name that starts with a ?
|
||||
const names = breadcrumbs
|
||||
.filter((breadcrumb) => breadcrumb.name.charAt(0) === '?')
|
||||
.map((breadcrumb) => breadcrumb.name.slice(1))
|
||||
// remove all names that are not in the names array
|
||||
for (const [route] of this.names) {
|
||||
if (!names.includes(route)) {
|
||||
this.names.delete(route)
|
||||
}
|
||||
}
|
||||
},
|
||||
setContext(context) {
|
||||
this.context = context
|
||||
},
|
||||
setRootContext(context) {
|
||||
this.rootContext = context
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useBreadcrumbs } from './breadcrumbs'
|
||||
import { useTheming } from './theme.ts'
|
||||
|
||||
export { useBreadcrumbs, useTheming }
|
||||
export { useTheming }
|
||||
|
||||
Reference in New Issue
Block a user