mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +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:
@@ -10,9 +10,10 @@ import {
|
||||
} from '@modrinth/api-client'
|
||||
import {
|
||||
ArrowBigUpDashIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
CompassIcon,
|
||||
HomeIcon,
|
||||
LeftArrowIcon,
|
||||
LibraryIcon,
|
||||
LogInIcon,
|
||||
LogOutIcon,
|
||||
@@ -46,6 +47,7 @@ import {
|
||||
provideNotificationManager,
|
||||
providePageContext,
|
||||
providePopupNotificationManager,
|
||||
TextLogo,
|
||||
useDebugLogger,
|
||||
useFormatBytes,
|
||||
useHostingIntercom,
|
||||
@@ -63,7 +65,6 @@ import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||
import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import AppActionBar from '@/components/ui/AppActionBar.vue'
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
||||
@@ -129,6 +130,7 @@ import {
|
||||
openAppUpdateChangelog,
|
||||
setAppUpdateActions,
|
||||
} from '@/providers/app-update.ts'
|
||||
import { createBreadcrumbManager, provideBreadcrumbManager } from '@/providers/breadcrumbs'
|
||||
import { createContentInstall, provideContentInstall } from '@/providers/content-install'
|
||||
import {
|
||||
provideAppUpdateDownloadProgress,
|
||||
@@ -151,6 +153,19 @@ import { appSettingsModalOpenProfileKey } from './providers/app-settings-modal'
|
||||
const themeStore = useTheming()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
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
|
||||
}
|
||||
|
||||
updateHistoryNavigationState()
|
||||
|
||||
const APP_LEFT_NAV_WIDTH = '4rem'
|
||||
const APP_SIDEBAR_WIDTH = 300
|
||||
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
|
||||
@@ -655,6 +670,7 @@ router.beforeEach(() => {
|
||||
routerToken = loading.begin()
|
||||
})
|
||||
router.afterEach((to, from, failure) => {
|
||||
updateHistoryNavigationState()
|
||||
trackEvent('PageView', {
|
||||
path: to.path,
|
||||
fromPath: from.path,
|
||||
@@ -1623,23 +1639,37 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</NavButton>
|
||||
</div>
|
||||
<div data-tauri-drag-region class="app-grid-statusbar bg-bg-raised h-[--top-bar-height] flex">
|
||||
<div data-tauri-drag-region class="flex min-w-0 flex-1 overflow-hidden p-3">
|
||||
<ModrinthAppLogo class="h-full w-auto shrink-0 text-contrast pointer-events-none" />
|
||||
<div data-tauri-drag-region class="flex shrink-0 items-center gap-1 ml-3">
|
||||
<div data-tauri-drag-region class="flex min-w-0 flex-1 items-center overflow-hidden p-2">
|
||||
<TextLogo class="h-7 w-auto shrink-0 text-contrast pointer-events-none" />
|
||||
<div data-tauri-drag-region class="ml-2 flex shrink-0 items-center gap-2">
|
||||
<ButtonStyled type="outlined" circular>
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
class="!h-7 !min-w-7 !w-7 !border !border-surface-4 !p-0 !opacity-100"
|
||||
:disabled="!canNavigateBack"
|
||||
aria-label="Go back"
|
||||
@click="router.back()"
|
||||
>
|
||||
<LeftArrowIcon />
|
||||
<ChevronLeftIcon
|
||||
class="!size-4 !text-primary"
|
||||
:class="{ 'opacity-20': !canNavigateBack }"
|
||||
/>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="outlined" circular>
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
class="!h-7 !min-w-7 !w-7 !border !border-surface-4 !p-0 !opacity-100"
|
||||
:disabled="!canNavigateForward"
|
||||
aria-label="Go forward"
|
||||
@click="router.forward()"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
<ChevronRightIcon
|
||||
class="!size-4 !text-primary"
|
||||
:class="{ 'opacity-20': !canNavigateForward }"
|
||||
/>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<Breadcrumbs class="pt-[2px]" />
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
<section data-tauri-drag-region class="flex shrink-0 ml-auto items-center">
|
||||
<ButtonStyled
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
ref="outerRef"
|
||||
data-tauri-drag-region
|
||||
class="min-w-0 overflow-hidden pl-3"
|
||||
class="min-w-0 overflow-hidden pl-4"
|
||||
:class="{ 'breadcrumb-fade-mask': isOverflowing }"
|
||||
:style="isOverflowing ? { '--scroll-distance': `-${overflowAmount}px` } : undefined"
|
||||
@mouseenter="onMouseEnter"
|
||||
@@ -11,30 +11,48 @@
|
||||
<div
|
||||
ref="innerRef"
|
||||
data-tauri-drag-region
|
||||
class="flex w-fit items-center gap-1"
|
||||
class="flex w-fit items-center gap-2 pr-4"
|
||||
:class="{ 'breadcrumbs-scroll': isAnimating }"
|
||||
@animationiteration="onAnimationIteration"
|
||||
>
|
||||
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
||||
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
|
||||
<router-link
|
||||
v-if="breadcrumb.link"
|
||||
:to="{
|
||||
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id as string)),
|
||||
query: breadcrumb.query,
|
||||
}"
|
||||
class="shrink-0 whitespace-nowrap text-primary"
|
||||
<template v-for="(breadcrumb, index) in breadcrumbs" :key="breadcrumb.slot">
|
||||
<component
|
||||
:is="index < breadcrumbs.length - 1 && breadcrumb.to ? RouterLink : 'span'"
|
||||
v-bind="index < breadcrumbs.length - 1 && breadcrumb.to ? { to: breadcrumb.to } : {}"
|
||||
:data-tauri-drag-region="index === breadcrumbs.length - 1 ? '' : undefined"
|
||||
class="flex shrink-0 items-center gap-1.5 whitespace-nowrap text-base font-medium leading-6"
|
||||
:class="
|
||||
index === breadcrumbs.length - 1
|
||||
? 'cursor-default select-none text-contrast'
|
||||
: 'text-primary hover:text-contrast'
|
||||
"
|
||||
:aria-current="index === breadcrumbs.length - 1 ? 'page' : undefined"
|
||||
>
|
||||
{{ resolveLabel(breadcrumb.name) }}
|
||||
</router-link>
|
||||
<span
|
||||
v-else
|
||||
<Avatar
|
||||
v-if="breadcrumb.visual?.type === 'image'"
|
||||
:src="breadcrumb.visual.src"
|
||||
:alt="breadcrumb.visual.alt ?? breadcrumb.label"
|
||||
:circle="breadcrumb.visual.circle"
|
||||
:tint-by="breadcrumb.visual.tintBy ?? breadcrumb.id"
|
||||
size="20px"
|
||||
no-shadow
|
||||
raised
|
||||
class="inline-block shrink-0 align-middle"
|
||||
:class="{ '!rounded-md': !breadcrumb.visual.circle }"
|
||||
/>
|
||||
<component
|
||||
:is="breadcrumb.visual.component"
|
||||
v-else-if="breadcrumb.visual?.type === 'icon'"
|
||||
class="size-5 shrink-0 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{ breadcrumb.label }}</span>
|
||||
</component>
|
||||
<ChevronRightIcon
|
||||
v-if="index < breadcrumbs.length - 1"
|
||||
data-tauri-drag-region
|
||||
class="shrink-0 whitespace-nowrap text-contrast font-semibold cursor-default select-none"
|
||||
>
|
||||
{{ resolveLabel(breadcrumb.name) }}
|
||||
</span>
|
||||
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5 shrink-0" />
|
||||
class="size-5 shrink-0 text-primary"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,34 +60,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon } from '@modrinth/assets'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Avatar } from '@modrinth/ui'
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
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)
|
||||
@@ -83,9 +80,13 @@ let stopping = false
|
||||
|
||||
function checkOverflow() {
|
||||
if (!outerRef.value || !innerRef.value) return
|
||||
const overflow = innerRef.value.scrollWidth - outerRef.value.clientWidth
|
||||
const outerStyles = window.getComputedStyle(outerRef.value)
|
||||
const horizontalPadding =
|
||||
Number.parseFloat(outerStyles.paddingLeft) + Number.parseFloat(outerStyles.paddingRight)
|
||||
const availableWidth = outerRef.value.clientWidth - horizontalPadding
|
||||
const overflow = innerRef.value.scrollWidth - availableWidth
|
||||
isOverflowing.value = overflow > 0
|
||||
overflowAmount.value = overflow + 12
|
||||
overflowAmount.value = overflow
|
||||
}
|
||||
|
||||
function onMouseEnter() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { SpinnerIcon } from '@modrinth/assets'
|
||||
import { Avatar, defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
@@ -14,6 +15,7 @@ const APPROX_USED_VERTICAL_SPACE = 513 // doesn't need to be exact lol just clos
|
||||
const STORAGE_KEY = 'modrinth-quick-instance-count'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -120,6 +122,10 @@ const onDividerPointerUp = (event) => {
|
||||
const getInstances = async () => {
|
||||
const instances = await list().catch(handleError)
|
||||
|
||||
for (const instance of instances) {
|
||||
queryClient.setQueryData(['instances', 'summary', instance.id], instance)
|
||||
}
|
||||
|
||||
allInstances.value = instances.sort((a, b) => {
|
||||
const dateACreated = dayjs(a.created)
|
||||
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
|
||||
|
||||
@@ -251,6 +251,9 @@
|
||||
"app.browse.back-to-instance": {
|
||||
"message": "Back to instance"
|
||||
},
|
||||
"app.browse.discover-project-type": {
|
||||
"message": "Discover {projectType}"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Discover servers"
|
||||
},
|
||||
@@ -803,6 +806,9 @@
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Back to discover"
|
||||
},
|
||||
"app.project.install-context.back-to-instance": {
|
||||
"message": "Back to instance"
|
||||
},
|
||||
"app.project.version.all-versions": {
|
||||
"message": "All versions"
|
||||
},
|
||||
|
||||
@@ -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 () => {
|
||||
watch([() => 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()
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
await refreshEnrichment()
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import { createContext } from '@modrinth/ui'
|
||||
import {
|
||||
type Component,
|
||||
computed,
|
||||
type ComputedRef,
|
||||
type MaybeRefOrGetter,
|
||||
shallowRef,
|
||||
toValue,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
export type BreadcrumbVisual =
|
||||
| {
|
||||
type: 'icon'
|
||||
component: Component
|
||||
}
|
||||
| {
|
||||
type: 'image'
|
||||
src?: string | null
|
||||
alt?: string
|
||||
circle?: boolean
|
||||
tintBy?: string | null
|
||||
}
|
||||
|
||||
export interface BreadcrumbDefinition {
|
||||
slot: string
|
||||
id: MaybeRefOrGetter<string>
|
||||
label: MaybeRefOrGetter<string>
|
||||
to?: MaybeRefOrGetter<RouteLocationRaw | undefined>
|
||||
visual?: MaybeRefOrGetter<BreadcrumbVisual | undefined>
|
||||
}
|
||||
|
||||
export interface ResolvedBreadcrumb {
|
||||
slot: string
|
||||
id: string
|
||||
label: string
|
||||
to?: RouteLocationRaw
|
||||
visual?: BreadcrumbVisual
|
||||
}
|
||||
|
||||
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),
|
||||
visual: definition.visual === undefined ? undefined : toValue(definition.visual),
|
||||
})),
|
||||
)
|
||||
|
||||
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,17 +16,11 @@ export default new createRouter({
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Pages.Index,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'Home' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/hosting/manage/',
|
||||
name: 'Servers',
|
||||
component: Pages.Servers,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'Servers' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/hosting/manage/:id',
|
||||
@@ -37,41 +31,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' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -79,34 +58,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: '',
|
||||
@@ -152,42 +118,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' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -197,59 +143,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',
|
||||
@@ -257,8 +174,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 }
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"decorations": true,
|
||||
"trafficLightPosition": {
|
||||
"x": 15.0,
|
||||
"y": 22.0
|
||||
"y": 26.0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -66,7 +66,6 @@ import _BugIcon from './icons/bug.svg?component'
|
||||
import _CalendarIcon from './icons/calendar.svg?component'
|
||||
import _CalendarArrowDownIcon from './icons/calendar-arrow-down.svg?component'
|
||||
import _CardIcon from './icons/card.svg?component'
|
||||
import _ChangeSkinIcon from './icons/change-skin.svg?component'
|
||||
import _ChartIcon from './icons/chart.svg?component'
|
||||
import _ChartAreaIcon from './icons/chart-area.svg?component'
|
||||
import _ChartColumnBigIcon from './icons/chart-column-big.svg?component'
|
||||
@@ -499,7 +498,6 @@ export const BugIcon = _BugIcon
|
||||
export const CalendarIcon = _CalendarIcon
|
||||
export const CalendarArrowDownIcon = _CalendarArrowDownIcon
|
||||
export const CardIcon = _CardIcon
|
||||
export const ChangeSkinIcon = _ChangeSkinIcon
|
||||
export const ChartIcon = _ChartIcon
|
||||
export const ChartAreaIcon = _ChartAreaIcon
|
||||
export const ChartColumnBigIcon = _ChartColumnBigIcon
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" transform="scale(-1 1)" viewBox="0 0 49.915 52.72">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.331" d="M15.71 31.484v19.07h18.63v-19.07l6.538 6.539 6.871-6.872-11.203-11.733H14.122L2.166 31.375l6.827 6.827z"/>
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.993" d="M24.872 19.548v-6.44"/>
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.331" d="M24.704 13.202a5.518 5.518 0 0 1-5.518-5.518 5.518 5.518 0 0 1 5.518-5.518 5.518 5.518 0 0 1 5.518 5.518"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 695 B |
@@ -107,7 +107,11 @@ function getProjectCardTags(result: Labrinth.Search.v3.ResultSearchProject, disp
|
||||
</template>
|
||||
<SelectedProjectsFloatingBar v-if="ctx.installContext?.value && ctx.variant !== 'web'" />
|
||||
|
||||
<NavTabs v-if="ctx.showProjectTypeTabs.value" :links="ctx.selectableProjectTypes.value" />
|
||||
<NavTabs
|
||||
v-if="ctx.showProjectTypeTabs.value"
|
||||
:links="ctx.selectableProjectTypes.value"
|
||||
:replace="ctx.variant === 'app'"
|
||||
/>
|
||||
|
||||
<StyledInput
|
||||
v-model="ctx.query.value"
|
||||
|
||||
@@ -733,6 +733,21 @@ const projects = computed<ResolvedProject[]>(() =>
|
||||
resolvedProjectType: resolveProjectType(project, tags?.loaders.value ?? []),
|
||||
})),
|
||||
)
|
||||
watch(
|
||||
() => projectsQuery.data.value,
|
||||
(projects) => {
|
||||
if (props.projectLinkMode !== 'app') return
|
||||
|
||||
for (const project of projects ?? []) {
|
||||
for (const identifier of [project.id, project.slug]) {
|
||||
if (identifier) {
|
||||
queryClient.setQueryData(['projects', 'summary', identifier], project)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
const organizations = computed(() => organizationsQuery.data.value ?? [])
|
||||
const collections = computed(() => collectionsQuery.data.value ?? [])
|
||||
const isBlocked = computed(() =>
|
||||
|
||||
Reference in New Issue
Block a user