Files
modrinth/packages/ui/src/layouts/shared/browse-tab/header.vue
T
Prospector b68a199814 menu refactor + web account switcher (#7307)
* add account switcher, migrate context menus to overflow menu system, update version filter controls, only pad instance icons

* app account switcher, improve some context menus, theme stuff

* prepr

* handle reauthentication

* fix a couple sign in issues, admin page cleanup, fix org status badges, fix official account badge, open in local keybind, publish plus icons

* rename generic menus to ButtonMenu (& types)

* fix hydration issue w/ dropdowns + middleware error
2026-08-27 00:35:34 +00:00

189 lines
5.4 KiB
Vue

<script setup lang="ts">
import { LeftArrowIcon, TagCategoryGamepad2Icon as Gamepad2Icon } from '@modrinth/assets'
import type { Component } from 'vue'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import Admonition from '#ui/components/base/Admonition.vue'
import Avatar from '#ui/components/base/Avatar.vue'
import { IconButton } from '#ui/components/base/buttons'
import PageHeader from '#ui/components/base/page-header/index.vue'
import PageHeaderMetadata from '#ui/components/base/page-header/metadata/index.vue'
import PageHeaderMetadataItem from '#ui/components/base/page-header/metadata/page-header-metadata-item.vue'
import TagIcon from '#ui/components/base/TagIcon.vue'
import { useServerImage } from '#ui/composables/use-server-image'
import { formatLoaderLabel } from '#ui/utils/loaders'
import SelectedProjectsLeaveModal from './components/SelectedProjectsLeaveModal.vue'
import { injectBrowseManager } from './providers/browse-manager'
import type { BrowseInstallContext } from './types'
const MEDAL_ICON_URL = 'https://cdn-raw.modrinth.com/medal_icon.webp'
const router = useRouter()
const props = defineProps<{
installContext?: BrowseInstallContext | null
divider?: boolean
bottomPadding?: boolean
}>()
type SelectedProjectsLeaveResult = 'cancel' | 'discard' | 'install'
type BrowseHeaderMetadataItem = {
id: string
label: string
icon?: Component
iconProps?: Record<string, unknown>
class?: string
}
const ctx = injectBrowseManager(null)
const installContext = computed(() => props.installContext ?? ctx?.installContext?.value ?? null)
const selectedProjectsLeaveModal = ref<InstanceType<typeof SelectedProjectsLeaveModal>>()
const serverId = computed(() => installContext.value?.serverId ?? '')
const upstream = computed(() => installContext.value?.upstream ?? null)
const { image: fetchedIcon } = useServerImage(serverId, upstream, {
enabled: computed(() => !!installContext.value?.serverId),
})
const iconSrc = computed(() => {
if (installContext.value?.isMedal) return MEDAL_ICON_URL
return fetchedIcon.value ?? installContext.value?.iconSrc ?? null
})
const isInstanceIcon = computed(() => !installContext.value?.serverId)
const metadataItems = computed(() => {
const context = installContext.value
if (!context) return []
const items: BrowseHeaderMetadataItem[] = []
if (context.heading) {
items.push({
id: 'heading',
label: context.heading,
class: '!text-primary',
})
}
if (context.gameVersion) {
items.push({
id: 'game-version',
label: `Minecraft ${context.gameVersion}`,
icon: Gamepad2Icon,
class: '!text-primary',
})
}
if (context.loader) {
const loaderName = formatLoaderLabel(context.loader)
const loaderLabel = [loaderName, context.loaderVersion].filter(Boolean).join(' ')
items.push({
id: 'loader',
label: loaderLabel,
icon: TagIcon,
iconProps: { tag: loaderName, enforceType: 'loader' },
class: '!text-primary',
})
}
return items
})
const selectedCount = computed(() => installContext.value?.selectedProjects?.length ?? 0)
const isInstallingSelected = computed(() => installContext.value?.isInstallingSelected ?? false)
async function handleBack() {
const context = installContext.value
if (!context) return
if (selectedCount.value > 0 && !isInstallingSelected.value) {
if (context.skipNonEssentialWarnings) {
await handleSelectedProjectsLeaveResult('discard', context)
return
}
const result = await selectedProjectsLeaveModal.value?.prompt()
await handleSelectedProjectsLeaveResult(result ?? 'cancel', context)
return
}
const shouldNavigate = await context.onBack?.()
if (shouldNavigate === false) return
await router.push(context.backUrl)
}
async function handleSelectedProjectsLeaveResult(
result: SelectedProjectsLeaveResult,
context: BrowseInstallContext,
) {
if (result === 'cancel') return
if (result === 'install') {
const shouldNavigate = await context.installSelected?.()
if (shouldNavigate === false) return
return
}
if (context.discardSelectedAndBack) {
await context.discardSelectedAndBack()
return
}
await (context.clearSelected ?? context.clearQueued)?.()
await router.push(context.backUrl)
}
</script>
<template>
<template v-if="installContext">
<SelectedProjectsLeaveModal
ref="selectedProjectsLeaveModal"
:count="selectedCount"
:installing="isInstallingSelected"
/>
<PageHeader
:title="installContext.name"
:divider="props.divider ?? false"
:bottom-padding="props.bottomPadding ?? false"
main-class="items-center"
title-class="leading-8"
truncate-title
>
<template #leading>
<IconButton
v-tooltip="installContext.backLabel"
size="xl"
:label="installContext.backLabel"
native-type="button"
@click="handleBack"
>
<LeftArrowIcon />
</IconButton>
<Avatar
v-if="iconSrc"
:src="iconSrc"
:alt="installContext.name"
size="48px"
class="shrink-0"
:pad-transparent-corners="isInstanceIcon"
/>
</template>
<template v-if="metadataItems.length" #metadata>
<PageHeaderMetadata>
<PageHeaderMetadataItem
v-for="item in metadataItems"
:key="item.id"
:icon="item.icon"
:icon-props="item.iconProps"
:class="item.class"
>
{{ item.label }}
</PageHeaderMetadataItem>
</PageHeaderMetadata>
</template>
</PageHeader>
<Admonition v-if="installContext.warning" type="warning" class="mt-4 mb-1">
{{ installContext.warning }}
</Admonition>
</template>
</template>