mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 10:34:53 +00:00
refactor: breadcrumbs system (#6912)
* refactor: breadcrumbs system * fix: qa * fix: qa * fix: qa * fix: qa * fix: loading flashes * fix: qa * fix: prepr * fix: qa * fix: change icon * use modrinth logo in header for compactness --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -3,9 +3,11 @@ import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
CheckIcon,
|
||||
ClipboardCopyIcon,
|
||||
CompassIcon,
|
||||
ExternalIcon,
|
||||
GlobeIcon,
|
||||
PlusIcon,
|
||||
ServerStackIcon,
|
||||
SpinnerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import type { BrowseInstallContentType, CardAction, ProjectType, Tags } from '@modrinth/ui'
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
commonMessages,
|
||||
CreationFlowModal,
|
||||
defineMessages,
|
||||
formatProjectTypeSentence,
|
||||
getLatestMatchingInstallVersion,
|
||||
getSelectedInstallPreferences,
|
||||
getTargetInstallPreferences,
|
||||
@@ -31,9 +34,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'
|
||||
@@ -52,13 +55,17 @@ import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata'
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { get_instance_worlds } from '@/helpers/worlds'
|
||||
import {
|
||||
type BreadcrumbDefinition,
|
||||
useBreadcrumb,
|
||||
useRootBreadcrumb,
|
||||
} from '@/providers/breadcrumbs'
|
||||
import { injectContentInstall } from '@/providers/content-install'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
import {
|
||||
createServerInstallContent,
|
||||
provideServerInstallContent,
|
||||
} from '@/providers/setup/server-install-content'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
@@ -71,6 +78,40 @@ 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 breadcrumbMessages = defineMessages({
|
||||
discoverProjectType: {
|
||||
id: 'app.browse.discover-project-type',
|
||||
defaultMessage: 'Discover {projectType}',
|
||||
},
|
||||
discoverServers: {
|
||||
id: 'app.browse.discover-servers',
|
||||
defaultMessage: 'Discover servers',
|
||||
},
|
||||
})
|
||||
const breadcrumbLabel = computed(() => {
|
||||
const browseRoute = displayedBrowseRoute.value
|
||||
if (browseRoute.query.from === 'worlds' || browseRoute.params.projectType === 'server') {
|
||||
return formatMessage(breadcrumbMessages.discoverServers)
|
||||
}
|
||||
|
||||
return formatMessage(breadcrumbMessages.discoverProjectType, {
|
||||
projectType: formatProjectTypeSentence(
|
||||
formatMessage,
|
||||
String(browseRoute.params.projectType ?? ''),
|
||||
2,
|
||||
),
|
||||
})
|
||||
})
|
||||
const themeStore = useTheming()
|
||||
const browseRouteActive = computed(() => route.path.startsWith('/browse/'))
|
||||
const serverSetupModalRef = ref<InstanceType<typeof CreationFlowModal> | null>(null)
|
||||
@@ -111,6 +152,86 @@ const {
|
||||
markServerProjectInstalled,
|
||||
} = serverInstallContent
|
||||
|
||||
type Instance = {
|
||||
game_version: string
|
||||
loader: string
|
||||
path: string
|
||||
install_stage: string
|
||||
icon_path?: string
|
||||
name: string
|
||||
link?: {
|
||||
type: string
|
||||
project_id: string
|
||||
version_id: string
|
||||
}
|
||||
}
|
||||
|
||||
const initialInstanceId = String(route.query.i ?? '')
|
||||
const instance: Ref<Instance | null> = ref(
|
||||
queryClient.getQueryData<Instance>(['instances', 'summary', initialInstanceId]) ?? null,
|
||||
)
|
||||
const installedProjectIds: Ref<string[] | null> = ref(null)
|
||||
const instanceHideInstalled = ref(false)
|
||||
const newlyInstalled = ref<string[]>([])
|
||||
const hiddenInstanceProjectIds = ref<Set<string>>(new Set())
|
||||
const hiddenInstanceProjectIdsInitialized = ref(false)
|
||||
const isServerInstance = ref(false)
|
||||
|
||||
const instanceBreadcrumb = route.query.i
|
||||
? useBreadcrumb({
|
||||
slot: 'instance',
|
||||
id: () => `instance:${String(displayedBrowseRoute.value.query.i ?? '')}`,
|
||||
label: () => instance.value?.name ?? formatMessage(commonMessages.loadingLabel),
|
||||
visual: () => ({
|
||||
type: 'image',
|
||||
src: instance.value?.icon_path ? convertFileSrc(instance.value.icon_path) : undefined,
|
||||
alt: instance.value?.name,
|
||||
tintBy: String(displayedBrowseRoute.value.query.i ?? ''),
|
||||
}),
|
||||
to: () => {
|
||||
const instancePath = `/instance/${encodeURIComponent(
|
||||
String(displayedBrowseRoute.value.query.i ?? ''),
|
||||
)}`
|
||||
return displayedBrowseRoute.value.query.from === 'worlds'
|
||||
? `${instancePath}/worlds`
|
||||
: instancePath
|
||||
},
|
||||
})
|
||||
: 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),
|
||||
visual: { type: 'icon', component: ServerStackIcon },
|
||||
to: serverBreadcrumbTo,
|
||||
})
|
||||
: undefined
|
||||
const breadcrumbParent = instanceBreadcrumb ?? 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,
|
||||
visual: { type: 'icon', component: CompassIcon },
|
||||
} satisfies BreadcrumbDefinition
|
||||
const browseBreadcrumb = breadcrumbParent
|
||||
? useBreadcrumb(breadcrumbDefinition, { parent: breadcrumbParent })
|
||||
: useRootBreadcrumb(breadcrumbDefinition)
|
||||
|
||||
debugLog('fetching tags (categories, loaders, gameVersions)')
|
||||
const [categories, loaders, availableGameVersions] = await Promise.all([
|
||||
get_categories()
|
||||
@@ -130,28 +251,6 @@ const tags: Ref<Tags> = computed(() => ({
|
||||
categories: categories.value ?? [],
|
||||
}))
|
||||
|
||||
type Instance = {
|
||||
game_version: string
|
||||
loader: string
|
||||
path: string
|
||||
install_stage: string
|
||||
icon_path?: string
|
||||
name: string
|
||||
link?: {
|
||||
type: string
|
||||
project_id: string
|
||||
version_id: string
|
||||
}
|
||||
}
|
||||
|
||||
const instance: Ref<Instance | null> = ref(null)
|
||||
const installedProjectIds: Ref<string[] | null> = ref(null)
|
||||
const instanceHideInstalled = ref(false)
|
||||
const newlyInstalled = ref<string[]>([])
|
||||
const hiddenInstanceProjectIds = ref<Set<string>>(new Set())
|
||||
const hiddenInstanceProjectIdsInitialized = ref(false)
|
||||
const isServerInstance = ref(false)
|
||||
|
||||
if (isFromWorlds.value && route.params.projectType !== 'server') {
|
||||
router.replace({
|
||||
path: '/browse/server',
|
||||
@@ -385,10 +484,6 @@ const messages = defineMessages({
|
||||
id: 'app.browse.add-to-an-instance',
|
||||
defaultMessage: 'Add to an instance',
|
||||
},
|
||||
discoverServers: {
|
||||
id: 'app.browse.discover-servers',
|
||||
defaultMessage: 'Discover servers',
|
||||
},
|
||||
environmentProvidedByServer: {
|
||||
id: 'search.filter.locked.server-environment.title',
|
||||
defaultMessage: 'Only client-side mods can be added to the server instance',
|
||||
@@ -444,31 +539,6 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const browseTitle = computed(() =>
|
||||
formatMessage(
|
||||
isFromWorlds.value ? messages.discoverServers : commonMessages.discoverContentLabel,
|
||||
),
|
||||
)
|
||||
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,
|
||||
})
|
||||
})
|
||||
|
||||
const projectType = ref<ProjectType>(route.params.projectType as ProjectType)
|
||||
|
||||
function resetInstanceContext() {
|
||||
@@ -482,8 +552,7 @@ function resetInstanceContext() {
|
||||
hiddenInstanceProjectIds.value = new Set()
|
||||
hiddenInstanceProjectIdsInitialized.value = false
|
||||
isServerInstance.value = false
|
||||
breadcrumbs.setName('BrowseTitle', formatMessage(commonMessages.discoverContentLabel))
|
||||
breadcrumbs.setContext(null)
|
||||
browseBreadcrumb.reset()
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -901,6 +970,14 @@ async function search(requestParams: string) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const hit of rawResults.result.hits) {
|
||||
for (const identifier of [hit.project_id, hit.slug]) {
|
||||
if (identifier) {
|
||||
queryClient.setQueryData(['projects', 'summary', identifier], hit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isServer) {
|
||||
const hits = rawResults.result.hits ?? []
|
||||
updateServerHits(hits)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { HomeIcon } from '@modrinth/assets'
|
||||
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 +11,17 @@ 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: '/',
|
||||
visual: { type: 'icon', component: HomeIcon },
|
||||
})
|
||||
|
||||
const instances = ref<GameInstance[]>([])
|
||||
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { ServerStackIcon } from '@modrinth/assets'
|
||||
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/',
|
||||
visual: { type: 'icon', component: ServerStackIcon },
|
||||
})
|
||||
|
||||
const { data: products } = useQuery({
|
||||
queryKey: ['billing', 'products'],
|
||||
queryFn: () => client.labrinth.billing_internal.getProducts(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
EyeIcon,
|
||||
LogInIcon,
|
||||
RotateCounterClockwiseIcon,
|
||||
ShirtIcon,
|
||||
SpinnerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
@@ -56,10 +57,19 @@ 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'
|
||||
import { appMessages } from '@/utils/app-messages'
|
||||
|
||||
useRootBreadcrumb({
|
||||
slot: 'root',
|
||||
id: 'skins',
|
||||
label: 'Skin selector',
|
||||
to: '/skins',
|
||||
visual: { type: 'icon', component: ShirtIcon },
|
||||
})
|
||||
|
||||
type UnlistenFn = () => void
|
||||
type VirtualSkinSectionListExpose = {
|
||||
getAddSkinButtonElement: () => HTMLElement | null | undefined
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { provideUserProfile, UserProfilePageLayout } from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, inject, watch } from 'vue'
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
|
||||
|
||||
import {
|
||||
@@ -29,12 +30,11 @@ import {
|
||||
unblock_user,
|
||||
} from '@/helpers/users'
|
||||
import { appSettingsModalOpenProfileKey } from '@/providers/app-settings-modal'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { useBreadcrumb } from '@/providers/breadcrumbs'
|
||||
|
||||
const route = useRoute()
|
||||
const openProfileSettings = inject(appSettingsModalOpenProfileKey, () => {})
|
||||
const queryClient = useQueryClient()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const userProfile = provideUserProfile({
|
||||
getUser: get_user_profile,
|
||||
getProjects: get_user_projects,
|
||||
@@ -55,17 +55,54 @@ const projectType = computed(() => {
|
||||
return Array.isArray(value) ? value[0] : value
|
||||
})
|
||||
|
||||
function getCachedUserSummary(id: string) {
|
||||
return queryClient.getQueryData<Labrinth.Users.v3.User>(['users', 'summary', id])
|
||||
}
|
||||
|
||||
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(getCachedUserSummary(userId.value)?.username ?? 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' },
|
||||
)
|
||||
|
||||
useBreadcrumb({
|
||||
slot: 'user',
|
||||
id: () => `user:${breadcrumbUserId.value}`,
|
||||
label: breadcrumbLabel,
|
||||
to: breadcrumbTo,
|
||||
visual: () => ({
|
||||
type: 'image',
|
||||
src: user.value?.avatar_url ?? getCachedUserSummary(breadcrumbUserId.value)?.avatar_url,
|
||||
alt: breadcrumbLabel.value,
|
||||
circle: true,
|
||||
tintBy: breadcrumbUserId.value,
|
||||
}),
|
||||
})
|
||||
|
||||
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.
|
||||
}
|
||||
@@ -87,8 +124,6 @@ async function ensureUserProfileData(id: string): Promise<void> {
|
||||
staleTime: 30_000,
|
||||
}),
|
||||
])
|
||||
|
||||
breadcrumbs.setName('User', breadcrumbName)
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate(async (to) => {
|
||||
@@ -97,21 +132,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>
|
||||
|
||||
@@ -48,15 +48,22 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import { injectAuth, injectModrinthClient, ServersManageRootLayout } from '@modrinth/ui'
|
||||
import { ServerStackIcon } from '@modrinth/assets'
|
||||
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,47 +72,63 @@ 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 ?? '')
|
||||
})
|
||||
|
||||
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.
|
||||
}
|
||||
function getCachedServerName(id: string): string | undefined {
|
||||
return queryClient
|
||||
.getQueryData<Archon.Servers.v0.ServerGetResponse>(['servers'])
|
||||
?.servers.find((server) => server.server_id === id)?.name
|
||||
}
|
||||
|
||||
const { data: serverData } = useQuery({
|
||||
queryKey: computed(() => ['servers', 'detail', serverId.value]),
|
||||
queryFn: () => null as unknown as Archon.Servers.v0.Server,
|
||||
enabled: false,
|
||||
queryFn: () => client.archon.servers_v0.get(serverId.value),
|
||||
enabled: computed(() => Boolean(serverId.value)),
|
||||
placeholderData: () =>
|
||||
queryClient
|
||||
.getQueryData<Archon.Servers.v0.ServerGetResponse>(['servers'])
|
||||
?.servers.find((server) => server.server_id === serverId.value),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const breadcrumbServerId = ref(serverId.value)
|
||||
const breadcrumbLabel = ref(
|
||||
getCachedServerName(serverId.value) ?? formatMessage(commonMessages.loadingLabel),
|
||||
)
|
||||
watch(
|
||||
serverId,
|
||||
(value) => {
|
||||
if (!route.path.startsWith('/hosting/manage/') || route.name === 'Servers') return
|
||||
breadcrumbServerId.value = value
|
||||
breadcrumbLabel.value = getCachedServerName(value) ?? formatMessage(commonMessages.loadingLabel)
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
)
|
||||
watch(
|
||||
serverData,
|
||||
(server) => {
|
||||
if (server?.name) {
|
||||
breadcrumbs.setName('Server', server.name)
|
||||
breadcrumbs.setContext({
|
||||
name: server.name,
|
||||
link: `/hosting/manage/${serverId.value}/content`,
|
||||
})
|
||||
}
|
||||
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,
|
||||
visual: { type: 'icon', component: ServerStackIcon },
|
||||
to: () => `/hosting/manage/${encodeURIComponent(breadcrumbServerId.value)}`,
|
||||
})
|
||||
provideBreadcrumbParent(serverBreadcrumb)
|
||||
|
||||
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'
|
||||
@@ -185,9 +192,10 @@ import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { createInstanceShortcut, showInstanceInFolder } from '@/helpers/utils.js'
|
||||
import { refreshWorlds, type ServerStatus } from '@/helpers/worlds'
|
||||
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { useBreadcrumbs, useTheming } from '@/store/state'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
import { provideSharedInstanceState, useSharedInstanceState } from './use-shared-instance-state'
|
||||
|
||||
@@ -198,10 +206,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'])
|
||||
@@ -214,7 +222,23 @@ window.addEventListener('online', () => {
|
||||
offline.value = false
|
||||
})
|
||||
|
||||
const instance = ref<GameInstance>()
|
||||
const initialInstanceId = String(displayedInstanceRoute.value.params.id ?? '')
|
||||
const instance = ref<GameInstance | undefined>(
|
||||
queryClient.getQueryData<GameInstance>(['instances', 'summary', initialInstanceId]),
|
||||
)
|
||||
useRootBreadcrumb({
|
||||
slot: 'instance',
|
||||
id: () => `instance:${String(displayedInstanceRoute.value.params.id ?? '')}`,
|
||||
label: () => instance.value?.name ?? formatMessage(commonMessages.loadingLabel),
|
||||
visual: () => ({
|
||||
type: 'image',
|
||||
src: instance.value?.icon_path ? convertFileSrc(instance.value.icon_path) : undefined,
|
||||
alt: instance.value?.name,
|
||||
tintBy: instance.value?.id ?? String(displayedInstanceRoute.value.params.id ?? ''),
|
||||
}),
|
||||
to: () => `/instance/${encodeURIComponent(String(displayedInstanceRoute.value.params.id ?? ''))}`,
|
||||
})
|
||||
|
||||
const preloadedContent = ref<InstanceContentData | null>(null)
|
||||
const playing = ref(false)
|
||||
const loading = ref(false)
|
||||
@@ -339,6 +363,9 @@ async function fetchInstance() {
|
||||
}
|
||||
|
||||
instance.value = nextInstance ?? undefined
|
||||
if (nextInstance) {
|
||||
queryClient.setQueryData(['instances', 'summary', nextInstance.id], nextInstance)
|
||||
}
|
||||
displayedInstanceRoute.value = nextRoute
|
||||
sharedInstanceState.reset()
|
||||
sharedInstanceState.refreshAvailability()
|
||||
@@ -503,20 +530,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) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { PlusIcon } from '@modrinth/assets'
|
||||
import { LibraryIcon, PlusIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, NavTabs } from '@modrinth/ui'
|
||||
import { inject, onUnmounted, ref, shallowRef } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
@@ -7,14 +7,19 @@ 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',
|
||||
visual: { type: 'icon', component: LibraryIcon },
|
||||
})
|
||||
|
||||
const instances = shallowRef(await list().catch(handleError))
|
||||
|
||||
|
||||
@@ -295,10 +295,10 @@ import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { getServerAddress } from '@/helpers/worlds'
|
||||
import { provideBreadcrumbParent, useBreadcrumb } from '@/providers/breadcrumbs'
|
||||
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 { useTheming } from '@/store/state.js'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
@@ -307,8 +307,29 @@ 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 projectBreadcrumbTo = computed(() => {
|
||||
const currentRoute = displayedProjectRoute.value
|
||||
if (currentRoute.name === 'Version') {
|
||||
return {
|
||||
name: 'Versions',
|
||||
params: { id: currentRoute.params.id },
|
||||
query: currentRoute.query,
|
||||
}
|
||||
}
|
||||
|
||||
return currentRoute.fullPath
|
||||
})
|
||||
const queryClient = useQueryClient()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const themeStore = useTheming()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -317,6 +338,10 @@ const messages = defineMessages({
|
||||
id: 'app.project.install-context.back-to-browse',
|
||||
defaultMessage: 'Back to discover',
|
||||
},
|
||||
backToInstance: {
|
||||
id: 'app.project.install-context.back-to-instance',
|
||||
defaultMessage: 'Back to instance',
|
||||
},
|
||||
alreadyInstalled: {
|
||||
id: 'app.project.install-button.already-installed',
|
||||
defaultMessage: 'This project is already installed',
|
||||
@@ -331,6 +356,40 @@ const { installingServerProjects, playServerProject, showAddServerToInstanceModa
|
||||
injectServerInstall()
|
||||
const installing = ref(false)
|
||||
const data = shallowRef(null)
|
||||
|
||||
function getProjectBreadcrumbSummary(projectId) {
|
||||
const identifier = Array.isArray(projectId) ? projectId[0] : projectId
|
||||
if (typeof identifier !== 'string' || !identifier) return undefined
|
||||
|
||||
return queryClient.getQueryData(['projects', 'summary', identifier])
|
||||
}
|
||||
|
||||
function getProjectBreadcrumbLabel(projectId) {
|
||||
const summary = getProjectBreadcrumbSummary(projectId)
|
||||
return summary?.name ?? summary?.title ?? formatMessage(commonMessages.loadingLabel)
|
||||
}
|
||||
|
||||
const projectBreadcrumbLabel = ref(getProjectBreadcrumbLabel(route.params.id))
|
||||
const projectBreadcrumb = useBreadcrumb({
|
||||
slot: 'project',
|
||||
id: () => `project:${String(displayedProjectRoute.value.params.id ?? '')}`,
|
||||
label: projectBreadcrumbLabel,
|
||||
visual: () => {
|
||||
const identifier = String(displayedProjectRoute.value.params.id ?? '')
|
||||
const loadedProject =
|
||||
data.value?.id === identifier || data.value?.slug === identifier ? data.value : undefined
|
||||
const project = loadedProject ?? getProjectBreadcrumbSummary(identifier)
|
||||
return {
|
||||
type: 'image',
|
||||
src: project?.icon_url,
|
||||
alt: projectBreadcrumbLabel.value,
|
||||
tintBy: identifier,
|
||||
}
|
||||
},
|
||||
to: projectBreadcrumbTo,
|
||||
})
|
||||
provideBreadcrumbParent(projectBreadcrumb)
|
||||
|
||||
const versions = shallowRef([])
|
||||
const members = shallowRef([])
|
||||
const categories = shallowRef([])
|
||||
@@ -410,9 +469,18 @@ const projectGalleryHref = computed(() => buildProjectHref(`/project/${route.par
|
||||
const projectBrowseBackUrl = computed(() => {
|
||||
const browsePath = route.query.b
|
||||
if (typeof browsePath === 'string' && browsePath.startsWith('/browse/')) return browsePath
|
||||
const instanceId = route.query.i
|
||||
if (typeof instanceId === 'string' && instanceId) {
|
||||
return `/instance/${encodeURIComponent(instanceId)}`
|
||||
}
|
||||
const type = data.value?.project_type ? `${data.value.project_type}` : 'mod'
|
||||
return buildBrowseHref(`/browse/${type}`)
|
||||
})
|
||||
const projectBackLabel = computed(() =>
|
||||
typeof route.query.i === 'string' && typeof route.query.b !== 'string'
|
||||
? formatMessage(messages.backToInstance)
|
||||
: formatMessage(messages.backToBrowse),
|
||||
)
|
||||
|
||||
const projectInstallContext = computed(() => {
|
||||
const serverData = serverInstallContent.serverContextServerData.value
|
||||
@@ -426,7 +494,7 @@ const projectInstallContext = computed(() => {
|
||||
iconSrc: null,
|
||||
isMedal: serverData.is_medal,
|
||||
backUrl: projectBrowseBackUrl.value,
|
||||
backLabel: formatMessage(messages.backToBrowse),
|
||||
backLabel: projectBackLabel.value,
|
||||
heading: serverInstallContent.serverBrowseHeading.value,
|
||||
queuedCount: serverInstallContent.queuedServerInstallCount.value,
|
||||
selectedProjects: serverInstallContent.selectedServerInstallProjects.value,
|
||||
@@ -446,7 +514,7 @@ const projectInstallContext = computed(() => {
|
||||
gameVersion: instance.value.game_version,
|
||||
iconSrc: instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : null,
|
||||
backUrl: projectBrowseBackUrl.value,
|
||||
backLabel: formatMessage(messages.backToBrowse),
|
||||
backLabel: projectBackLabel.value,
|
||||
heading: formatMessage(commonMessages.installingContentLabel),
|
||||
}
|
||||
}
|
||||
@@ -607,6 +675,7 @@ function reportProject() {
|
||||
}
|
||||
|
||||
async function fetchProjectData() {
|
||||
projectBreadcrumbLabel.value = getProjectBreadcrumbLabel(route.params.id)
|
||||
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 +688,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),
|
||||
@@ -628,6 +698,14 @@ async function fetchProjectData() {
|
||||
route.query.i ? getInstanceProjects(route.query.i).catch(handleError) : Promise.resolve(),
|
||||
])
|
||||
|
||||
for (const member of members.value ?? []) {
|
||||
for (const identifier of [member.user.id, member.user.username]) {
|
||||
if (identifier) {
|
||||
queryClient.setQueryData(['users', 'summary', identifier], member.user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
versions.value = versions.value.sort((a, b) => dayjs(b.date_published) - dayjs(a.date_published))
|
||||
|
||||
if (instanceProjects.value) {
|
||||
@@ -647,8 +725,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ import {
|
||||
ExternalIcon,
|
||||
MoreVerticalIcon,
|
||||
ReportIcon,
|
||||
VersionIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
@@ -95,12 +96,12 @@ import {
|
||||
useVIntl,
|
||||
VersionPage,
|
||||
} from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { computed, 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 +120,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 +144,19 @@ 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 versionBreadcrumbLabel = computed(() => {
|
||||
const versionNumber = version.value?.version_number
|
||||
const versionLabel = formatMessage(commonMessages.versionLabel)
|
||||
return versionNumber ? `${versionLabel} ${versionNumber}` : versionLabel
|
||||
})
|
||||
useBreadcrumb({
|
||||
slot: 'project-version',
|
||||
id: () =>
|
||||
`version:${props.project.id}:${String(displayedVersionRoute.value.params.version ?? '')}`,
|
||||
label: versionBreadcrumbLabel,
|
||||
visual: { type: 'icon', component: VersionIcon },
|
||||
to: () => displayedVersionRoute.value.fullPath,
|
||||
})
|
||||
|
||||
const enrichment = ref<Labrinth.Projects.v2.DependencyInfo | undefined>(undefined)
|
||||
const enrichmentLoading = ref(false)
|
||||
@@ -202,18 +223,12 @@ async function refreshEnrichment() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.versions,
|
||||
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()
|
||||
}
|
||||
},
|
||||
)
|
||||
watch([() => props.versions, () => route.params.version], async () => {
|
||||
if (route.params.version) {
|
||||
version.value = props.versions.find((v) => v.id === route.params.version)
|
||||
await refreshEnrichment()
|
||||
}
|
||||
})
|
||||
|
||||
await refreshEnrichment()
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user