refactor: introduce PageHeader component (#6629)

* refactor: introduce PageHeader component

* feat: split up according to component structure guide

* fix: lint

* fix: label inconsistencies

* feat: refactor PageHeader (again)

* refactor: old impls

* fix: changes

* fix: rescan

* fix: prepr

* fix: dedupe

* fix: rev

* fix: lint

* fix: lint

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-07-20 18:25:12 +00:00
committed by GitHub
co-authored by Prospector
parent 8b6f0ec695
commit 932858b5f5
65 changed files with 3131 additions and 2091 deletions
@@ -0,0 +1,294 @@
<template>
<PageHeader :title="instance.name">
<template #leading>
<Avatar :src="iconSrc" :alt="instance.name" size="64px" :tint-by="instance.id" />
</template>
<template #metadata>
<InstanceHeaderServerMetadata
v-if="isServerInstance"
:loading-server-ping="loadingServerPing"
:players-online="playersOnline"
:status-online="statusOnline"
:recent-plays="recentPlays"
:ping="ping"
:minecraft-server="minecraftServer"
:linked-project-v3="linkedProjectV3"
:instance-id="instance.id"
/>
<PageHeaderMetadata v-else>
<PageHeaderMetadataItem :icon="Gamepad2Icon" tooltip="Minecraft version">
Minecraft {{ instance.game_version }}
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
:icon="ServerLoaderIcon"
:icon-props="{ loader: loaderDisplayName }"
tooltip="Mod loader"
>
{{ loaderLabel }}
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
v-if="showInstancePlayTime"
:icon="TimerIcon"
tooltip="Total playtime"
>
{{ playtimeLabel }}
</PageHeaderMetadataItem>
</PageHeaderMetadata>
</template>
<template #actions>
<PageHeaderActions>
<ButtonStyled v-if="isInstalling" color="brand" size="large">
<button type="button" disabled>
{{ formatMessage(commonMessages.installingLabel) }}
</button>
</ButtonStyled>
<ButtonStyled v-else-if="instance.install_stage !== 'installed'" color="brand" size="large">
<button type="button" @click="emit('repair')">
<DownloadIcon />
{{ formatMessage(messages.repair) }}
</button>
</ButtonStyled>
<ButtonStyled v-else-if="playing" color="red" size="large">
<button type="button" :disabled="stopping" @click="emit('stop')">
<StopCircleIcon />
{{
stopping ? formatMessage(messages.stopping) : formatMessage(commonMessages.stopButton)
}}
</button>
</ButtonStyled>
<JoinedButtons
v-else-if="!loading && isServerInstance"
:actions="serverPlayActions"
color="brand"
size="large"
/>
<ButtonStyled v-else-if="!loading" color="brand" size="large">
<button type="button" @click="emit('play')">
<PlayIcon />
{{ formatMessage(commonMessages.playButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-else color="brand" size="large">
<button type="button" disabled>{{ formatMessage(messages.starting) }}</button>
</ButtonStyled>
<ButtonStyled circular size="large">
<button
v-tooltip="formatMessage(messages.instanceSettings)"
type="button"
:aria-label="formatMessage(messages.instanceSettings)"
@click="emit('settings')"
>
<SettingsIcon />
</button>
</ButtonStyled>
<ButtonStyled circular size="large" type="transparent">
<TeleportOverflowMenu
:options="moreActions"
:tooltip="formatMessage(messages.moreActions)"
:aria-label="formatMessage(messages.moreActions)"
>
<MoreVerticalIcon />
</TeleportOverflowMenu>
</ButtonStyled>
</PageHeaderActions>
</template>
</PageHeader>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
DownloadIcon,
ExternalIcon,
FolderOpenIcon,
MoreVerticalIcon,
PackageIcon,
PlayIcon,
SettingsIcon,
StopCircleIcon,
TagCategoryGamepad2Icon as Gamepad2Icon,
TimerIcon,
} from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
commonMessages,
defineMessages,
formatLoaderLabel,
type JoinedButtonAction,
JoinedButtons,
LoaderIcon as ServerLoaderIcon,
PageHeader,
PageHeaderActions,
PageHeaderMetadata,
PageHeaderMetadataItem,
type ServerLoader,
TeleportOverflowMenu,
type TeleportOverflowMenuItem,
useVIntl,
} from '@modrinth/ui'
import { computed } from 'vue'
import type { GameInstance } from '@/helpers/types'
import InstanceHeaderServerMetadata from './instance-page-header-server-metadata.vue'
const messages = defineMessages({
createShortcut: {
id: 'instance.action.create-shortcut',
defaultMessage: 'Create shortcut',
},
exportModpack: {
id: 'instance.action.export-modpack',
defaultMessage: 'Export modpack',
},
instanceSettings: {
id: 'instance.action.settings',
defaultMessage: 'Instance settings',
},
launchInstance: {
id: 'instance.action.launch-instance',
defaultMessage: 'Launch instance',
},
moreActions: {
id: 'instance.action.more-actions',
defaultMessage: 'More actions',
},
neverPlayed: {
id: 'instance.playtime.never-played',
defaultMessage: 'Never played',
},
openFolder: {
id: 'instance.action.open-folder',
defaultMessage: 'Open folder',
},
repair: {
id: 'instance.action.repair',
defaultMessage: 'Repair',
},
starting: {
id: 'instance.action.starting',
defaultMessage: 'Starting...',
},
stopping: {
id: 'instance.action.stopping',
defaultMessage: 'Stopping...',
},
})
const props = withDefaults(
defineProps<{
instance: GameInstance
iconSrc?: string | null
isServerInstance?: boolean
showInstancePlayTime?: boolean
timePlayed?: number
playing?: boolean
loading?: boolean
stopping?: boolean
loadingServerPing?: boolean
playersOnline?: number
statusOnline?: boolean
recentPlays?: number
ping?: number
minecraftServer?: Labrinth.Projects.v3.Project['minecraft_server']
linkedProjectV3?: Labrinth.Projects.v3.Project
}>(),
{
iconSrc: null,
isServerInstance: false,
showInstancePlayTime: false,
timePlayed: 0,
playing: false,
loading: false,
stopping: false,
loadingServerPing: false,
playersOnline: undefined,
statusOnline: false,
recentPlays: undefined,
ping: undefined,
minecraftServer: undefined,
linkedProjectV3: undefined,
},
)
const emit = defineEmits<{
repair: []
stop: []
play: []
playServer: []
settings: []
openFolder: []
export: []
createShortcut: []
}>()
const installingStages = [
'installing',
'pack_installing',
'pack_installed',
'not_installed',
'minecraft_installing',
]
const { formatMessage } = useVIntl()
const isInstalling = computed(() => installingStages.includes(props.instance.install_stage))
const loaderDisplayName = computed(() => formatLoaderLabel(props.instance.loader) as ServerLoader)
const loaderLabel = computed(() =>
[loaderDisplayName.value, props.instance.loader_version].filter(Boolean).join(' '),
)
const playtimeLabel = computed(() => {
if (props.timePlayed <= 0) return formatMessage(messages.neverPlayed)
const hours = Math.floor(props.timePlayed / 3600)
if (hours >= 1) {
return `${hours} hour${hours > 1 ? 's' : ''}`
}
const minutes = Math.floor(props.timePlayed / 60)
if (minutes >= 1) {
return `${minutes} minute${minutes > 1 ? 's' : ''}`
}
const seconds = Math.floor(props.timePlayed)
return `${seconds} second${seconds > 1 ? 's' : ''}`
})
const serverPlayActions = computed<JoinedButtonAction[]>(() => [
{
id: 'join_server',
label: formatMessage(commonMessages.playButton),
icon: PlayIcon,
action: () => emit('playServer'),
},
{
id: 'launch_instance',
label: formatMessage(messages.launchInstance),
icon: PlayIcon,
action: () => emit('play'),
},
])
const moreActions = computed<TeleportOverflowMenuItem[]>(() => [
{
id: 'open-folder',
label: formatMessage(messages.openFolder),
icon: FolderOpenIcon,
action: () => emit('openFolder'),
},
{
id: 'export-mrpack',
label: formatMessage(messages.exportModpack),
icon: PackageIcon,
action: () => emit('export'),
},
{
id: 'create-shortcut',
label: formatMessage(messages.createShortcut),
icon: ExternalIcon,
action: () => emit('createShortcut'),
},
])
</script>
@@ -0,0 +1,63 @@
<template>
<div class="flex items-center flex-wrap gap-2">
<template v-if="loadingServerPing">
<ServerOnlinePlayers
v-if="playersOnline !== undefined"
:online="playersOnline"
:status-online="statusOnline"
hide-label
/>
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
<div
v-if="
(playersOnline !== undefined || recentPlays !== undefined) &&
(minecraftServer?.region || ping)
"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
<ServerPing v-if="ping" :ping="ping" />
</template>
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
<div v-if="minecraftServer?.region || ping" class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
<div v-if="linkedProjectV3" class="flex gap-1.5 items-center font-medium text-primary">
Linked to
<Avatar
:src="linkedProjectV3.icon_url"
:alt="linkedProjectV3.name"
:tint-by="instanceId"
size="24px"
/>
<router-link
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
class="hover:underline text-primary truncate"
>
{{ linkedProjectV3.name }}
</router-link>
</div>
</div>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
Avatar,
ServerOnlinePlayers,
ServerPing,
ServerRecentPlays,
ServerRegion,
} from '@modrinth/ui'
defineProps<{
loadingServerPing?: boolean
playersOnline?: number
statusOnline?: boolean
recentPlays?: number
ping?: number
minecraftServer?: Labrinth.Projects.v3.Project['minecraft_server']
linkedProjectV3?: Labrinth.Projects.v3.Project
instanceId?: string
}>()
</script>
@@ -788,6 +788,33 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Sign in to a Modrinth account</link> to add friends and see what they're playing!"
},
"instance.action.create-shortcut": {
"message": "Create shortcut"
},
"instance.action.export-modpack": {
"message": "Export modpack"
},
"instance.action.launch-instance": {
"message": "Launch instance"
},
"instance.action.more-actions": {
"message": "More actions"
},
"instance.action.open-folder": {
"message": "Open folder"
},
"instance.action.repair": {
"message": "Repair"
},
"instance.action.settings": {
"message": "Instance settings"
},
"instance.action.starting": {
"message": "Starting..."
},
"instance.action.stopping": {
"message": "Stopping..."
},
"instance.add-server.add-and-play": {
"message": "Add and play"
},
@@ -830,6 +857,9 @@
"instance.files.save-as": {
"message": "Save as..."
},
"instance.playtime.never-played": {
"message": "Never played"
},
"instance.server-modal.address": {
"message": "Address"
},
+27 -239
View File
@@ -13,207 +13,31 @@
@unlinked="fetchInstance"
/>
<UpdateToPlayModal ref="updateToPlayModal" :instance="instance" />
<ContentPageHeader>
<template #icon>
<Avatar
:src="icon ? icon : undefined"
:alt="instance.name"
size="64px"
:tint-by="instance.id"
/>
</template>
<template #title>
{{ instance.name }}
</template>
<template #stats>
<div class="flex items-center flex-wrap gap-2">
<template v-if="!isServerInstance">
<div class="flex items-center gap-2 capitalize font-medium">
{{ instance.loader }} {{ instance.game_version }}
</div>
<template v-if="showInstancePlayTime">
<div class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
<div class="flex items-center gap-2 font-medium">
<template v-if="timePlayed > 0">
{{ timePlayedHumanized }}
</template>
<template v-else> Never played </template>
</div>
</template>
</template>
<template v-else>
<template v-if="loadingServerPing">
<ServerOnlinePlayers
v-if="playersOnline !== undefined"
:online="playersOnline"
:status-online="statusOnline"
hide-label
/>
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
<div
v-if="
(playersOnline !== undefined || recentPlays !== undefined) &&
(minecraftServer?.region || ping)
"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
<ServerPing v-if="ping" :ping="ping" />
</template>
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
<div
v-if="minecraftServer?.region || ping"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
<div
v-if="linkedProjectV3"
class="flex gap-1.5 items-center font-medium text-primary"
>
Linked to
<Avatar
:src="linkedProjectV3.icon_url"
:alt="linkedProjectV3.name"
:tint-by="instance.id"
size="24px"
/>
<router-link
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
class="hover:underline text-primary truncate"
>
{{ linkedProjectV3.name }}
</router-link>
</div>
</template>
</div>
</template>
<template #actions>
<div class="flex gap-2">
<ButtonStyled
v-if="
[
'installing',
'pack_installing',
'pack_installed',
'not_installed',
'minecraft_installing',
].includes(instance.install_stage)
"
color="brand"
size="large"
>
<button disabled>Installing...</button>
</ButtonStyled>
<ButtonStyled
v-else-if="instance.install_stage !== 'installed'"
color="brand"
size="large"
>
<button @click="repairInstance()">
<DownloadIcon />
Repair
</button>
</ButtonStyled>
<ButtonStyled v-else-if="playing === true" color="red" size="large">
<button :disabled="stopping" @click="stopInstance('InstancePage')">
<StopCircleIcon />
{{ stopping ? 'Stopping...' : 'Stop' }}
</button>
</ButtonStyled>
<ButtonStyled
v-else-if="playing === false && loading === false && !isServerInstance"
color="brand"
size="large"
>
<button @click="startInstance('InstancePage')">
<PlayIcon />
Play
</button>
</ButtonStyled>
<div
v-else-if="playing === false && loading === false && isServerInstance"
class="joined-buttons"
>
<ButtonStyled color="brand" size="large">
<button @click="handlePlayServer()">
<PlayIcon />
Play
</button>
</ButtonStyled>
<ButtonStyled color="brand" size="large">
<OverflowMenu
:options="[
{
id: 'join_server',
action: () => handlePlayServer(),
},
{
id: 'launch_instance',
action: () => startInstance('InstancePage'),
},
]"
>
<div class="w-0 text-xl relative top-0.5 right-2.5">
<DropdownIcon />
</div>
<template #join_server>
<PlayIcon />
Join server
</template>
<template #launch_instance>
<PlayIcon />
Launch instance
</template>
</OverflowMenu>
</ButtonStyled>
</div>
<ButtonStyled
v-else-if="loading === true && playing === false"
color="brand"
size="large"
>
<button disabled>Starting...</button>
</ButtonStyled>
<ButtonStyled circular size="large">
<button v-tooltip="'Instance settings'" @click="settingsModal?.show()">
<SettingsIcon />
</button>
</ButtonStyled>
<ButtonStyled type="transparent" circular size="large">
<OverflowMenu
:options="[
{
id: 'open-folder',
action: () => {
if (instance) showInstanceInFolder(instance.id)
},
},
{
id: 'export-mrpack',
action: () => exportModal?.show(),
},
{
id: 'create-shortcut',
action: () => createShortcut(),
},
]"
>
<MoreVerticalIcon />
<template #share-instance> <UserPlusIcon /> Share instance </template>
<template #host-a-server> <ServerIcon /> Create a server </template>
<template #open-folder> <FolderOpenIcon /> Open folder </template>
<template #export-mrpack> <PackageIcon /> Export modpack </template>
<template #create-shortcut> <ExternalIcon /> Create shortcut </template>
</OverflowMenu>
</ButtonStyled>
</div>
</template>
</ContentPageHeader>
<InstancePageHeader
:instance="instance"
:icon-src="icon"
:is-server-instance="isServerInstance"
:show-instance-play-time="showInstancePlayTime"
:time-played="timePlayed"
:playing="playing"
:loading="loading"
:stopping="stopping"
:loading-server-ping="loadingServerPing"
:players-online="playersOnline"
:status-online="statusOnline"
:recent-plays="recentPlays"
:ping="ping"
:minecraft-server="minecraftServer"
:linked-project-v3="linkedProjectV3"
@repair="() => repairInstance()"
@stop="() => stopInstance('InstancePage')"
@play="() => startInstance('InstancePage')"
@play-server="() => handlePlayServer()"
@settings="() => settingsModal?.show()"
@open-folder="() => instance && showInstanceInFolder(instance.id)"
@export="() => exportModal?.show()"
@create-shortcut="() => createShortcut()"
/>
</div>
<div :class="['px-6', { 'shrink-0': isFixedRender }]">
<NavTabs :links="tabs" />
@@ -272,49 +96,30 @@ import {
BoxesIcon,
CheckCircleIcon,
ClipboardCopyIcon,
DownloadIcon,
DropdownIcon,
EditIcon,
ExternalIcon,
EyeIcon,
FolderOpenIcon,
GlobeIcon,
HashIcon,
MoreVerticalIcon,
PackageIcon,
PlayIcon,
PlusIcon,
ServerIcon,
SettingsIcon,
StopCircleIcon,
TerminalSquareIcon,
UpdatedIcon,
UserPlusIcon,
XIcon,
} from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
ContentPageHeader,
injectNotificationManager,
NavTabs,
OverflowMenu,
ServerOnlinePlayers,
ServerPing,
ServerRecentPlays,
ServerRegion,
useLoadingBarToken,
} from '@modrinth/ui'
import { injectNotificationManager, NavTabs, useLoadingBarToken } from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime'
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import ExportModal from '@/components/ui/ExportModal.vue'
import InstancePageHeader from '@/components/ui/instance-page-header/index.vue'
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import {
@@ -336,7 +141,6 @@ import { injectServerInstall } from '@/providers/server-install'
import { handleSevereError } from '@/store/error.js'
import { useBreadcrumbs, useTheming } from '@/store/state'
dayjs.extend(duration)
dayjs.extend(relativeTime)
const { addNotification, handleError } = injectNotificationManager()
@@ -758,22 +562,6 @@ const timePlayed = computed(() => {
: 0
})
const timePlayedHumanized = computed(() => {
const duration = dayjs.duration(timePlayed.value, 'seconds')
const hours = Math.floor(duration.asHours())
if (hours >= 1) {
return hours + ' hour' + (hours > 1 ? 's' : '')
}
const minutes = Math.floor(duration.asMinutes())
if (minutes >= 1) {
return minutes + ' minute' + (minutes > 1 ? 's' : '')
}
const seconds = Math.floor(duration.asSeconds())
return seconds + ' second' + (seconds > 1 ? 's' : '')
})
onUnmounted(() => {
unlistenProcesses()
unlistenInstances()
+170 -146
View File
@@ -59,142 +59,100 @@
>
<ProjectBackgroundGradient :project="data" />
</Teleport>
<ProjectHeader
<ProjectPageHeader
v-else
:project="data"
:project-v3="projectV3"
:ping="serverPing"
:show-status-badge="data.status !== 'approved'"
@contextmenu.prevent.stop="handleRightClick"
@category="(category) => router.push(`${projectSearchUrl}?f=categories:${category}`)"
>
<template v-if="isServerProject" #actions>
<ButtonStyled v-if="serverPlaying" size="large" color="red">
<button @click="handleStopServer">
<StopCircleIcon />
{{ formatMessage(commonMessages.stopButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-else size="large" color="brand">
<button
:disabled="data && installingServerProjects.includes(data.id)"
@click="handleClickPlay"
>
<PlayIcon />
{{
data && installingServerProjects.includes(data.id)
? formatMessage(commonMessages.installingLabel)
: formatMessage(commonMessages.playButton)
}}
</button>
</ButtonStyled>
<ButtonStyled size="large" circular>
<button
v-tooltip="formatMessage(commonMessages.addServerToInstanceButton)"
@click="handleAddServerToInstance"
>
<PlusIcon />
</button>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:tooltip="`More options`"
:options="[
{
id: 'open-in-browser',
link: `https://modrinth.com/project/${data.slug}`,
external: true,
},
{
divider: true,
},
{
id: 'report',
color: 'red',
hoverFilled: true,
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
},
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #open-in-browser>
<ExternalIcon /> {{ formatMessage(commonMessages.openInBrowserButton) }}
</template>
<template #report> <ReportIcon /> Report </template>
</OverflowMenu>
</ButtonStyled>
<template #actions>
<template v-if="isServerProject">
<ButtonStyled v-if="serverPlaying" color="red" size="large">
<button type="button" @click="handleStopServer">
<StopCircleIcon />
{{ formatMessage(commonMessages.stopButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-else color="brand" size="large">
<button type="button" :disabled="serverInstallLoading" @click="handleClickPlay">
<PlayIcon />
{{
serverInstallLoading
? formatMessage(commonMessages.installingLabel)
: formatMessage(commonMessages.playButton)
}}
</button>
</ButtonStyled>
<ButtonStyled circular size="large">
<button
v-tooltip="formatMessage(commonMessages.addServerToInstanceButton)"
type="button"
:aria-label="formatMessage(commonMessages.addServerToInstanceButton)"
@click="handleAddServerToInstance"
>
<PlusIcon />
</button>
</ButtonStyled>
<ButtonStyled circular size="large" type="transparent">
<TeleportOverflowMenu
:options="serverProjectHeaderMoreActions"
tooltip="More options"
aria-label="More options"
>
<MoreVerticalIcon />
</TeleportOverflowMenu>
</ButtonStyled>
</template>
<template v-else>
<ButtonStyled v-if="showSwitchVersion && onVersionsPage" size="large">
<button v-tooltip="formatMessage(messages.alreadyInstalled)" type="button" disabled>
<CheckIcon />
{{ formatMessage(commonMessages.installedLabel) }}
</button>
</ButtonStyled>
<ButtonStyled v-else-if="showSwitchVersion" size="large">
<button type="button" @click="goToVersions">
<SwapIcon />
{{ formatMessage(messages.switchVersion) }}
</button>
</ButtonStyled>
<ButtonStyled v-else color="brand" size="large">
<button
v-tooltip="
installButtonInstalled ? formatMessage(messages.alreadyInstalled) : undefined
"
type="button"
:disabled="installButtonDisabled"
@click="install(null)"
>
<component :is="installButtonIcon" :class="installButtonIconClass" />
{{
installButtonInstalled
? formatMessage(commonMessages.installedLabel)
: installButtonValidating
? formatMessage(commonMessages.validatingLabel)
: installButtonLoading
? formatMessage(commonMessages.installingLabel)
: serverProjectSelected
? formatMessage(commonMessages.selectedLabel)
: formatMessage(commonMessages.installButton)
}}
</button>
</ButtonStyled>
<ButtonStyled circular size="large" type="transparent">
<TeleportOverflowMenu
:options="projectHeaderMoreActions"
tooltip="More options"
aria-label="More options"
>
<MoreVerticalIcon />
</TeleportOverflowMenu>
</ButtonStyled>
</template>
</template>
<template v-else #actions>
<ButtonStyled v-if="showSwitchVersion && onVersionsPage" size="large">
<button v-tooltip="installButtonTooltip" disabled>
<CheckIcon />
{{ formatMessage(commonMessages.installedLabel) }}
</button>
</ButtonStyled>
<ButtonStyled v-else-if="showSwitchVersion" size="large">
<button @click="goToVersions">
<SwapIcon />
{{ formatMessage(messages.switchVersion) }}
</button>
</ButtonStyled>
<ButtonStyled v-else size="large" color="brand">
<button
v-tooltip="installButtonTooltip"
:disabled="installButtonDisabled"
@click="install(null)"
>
<SpinnerIcon
v-if="installButtonLoading && !installButtonInstalled"
class="animate-spin"
/>
<DownloadIcon v-else-if="!installButtonInstalled && !serverProjectSelected" />
<CheckIcon v-else />
{{ installButtonLabel }}
</button>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:tooltip="`More options`"
:options="[
{
id: 'follow',
disabled: true,
tooltip: 'Coming soon',
action: () => {},
},
{
id: 'save',
disabled: true,
tooltip: 'Coming soon',
action: () => {},
},
{
id: 'open-in-browser',
link: `https://modrinth.com/${data.project_type}/${data.slug}`,
external: true,
},
{
divider: true,
},
{
id: 'report',
color: 'red',
hoverFilled: true,
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
},
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #open-in-browser>
<ExternalIcon /> {{ formatMessage(commonMessages.openInBrowserButton) }}
</template>
<template #follow> <HeartIcon /> Follow </template>
<template #save> <BookmarkIcon /> Save </template>
<template #report> <ReportIcon /> Report </template>
</OverflowMenu>
</ButtonStyled>
</template>
</ProjectHeader>
</ProjectPageHeader>
<NavTabs
:links="[
{
@@ -289,9 +247,8 @@ import {
getTargetInstallPreferences,
injectNotificationManager,
NavTabs,
OverflowMenu,
ProjectBackgroundGradient,
ProjectHeader,
ProjectPageHeader,
ProjectSidebarCompatibility,
ProjectSidebarCreators,
ProjectSidebarDetails,
@@ -300,6 +257,7 @@ import {
ProjectSidebarTags,
requestInstall,
SelectedProjectsFloatingBar,
TeleportOverflowMenu,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
@@ -358,14 +316,14 @@ const messages = defineMessages({
id: 'app.project.install-context.back-to-browse',
defaultMessage: 'Back to discover',
},
installContentToInstance: {
id: 'app.project.install-context.install-content-to-instance',
defaultMessage: 'Install content to instance',
},
alreadyInstalled: {
id: 'app.project.install-button.already-installed',
defaultMessage: 'This project is already installed',
},
installContentToInstance: {
id: 'app.project.install-context.install-content-to-instance',
defaultMessage: 'Install content to instance',
},
switchVersion: {
id: 'app.project.install-button.switch-version',
defaultMessage: 'Switch version',
@@ -529,17 +487,72 @@ const installButtonInstalled = computed(() =>
const installButtonDisabled = computed(
() => installButtonInstalled.value || installButtonLoading.value,
)
const installButtonLabel = computed(() => {
if (installButtonInstalled.value) return formatMessage(commonMessages.installedLabel)
if (installButtonValidating.value) return formatMessage(commonMessages.validatingLabel)
if (installButtonLoading.value) return formatMessage(commonMessages.installingLabel)
if (serverProjectSelected.value) return formatMessage(commonMessages.selectedLabel)
return formatMessage(commonMessages.installButton)
})
const installButtonTooltip = computed(() => {
if (installButtonInstalled.value) return formatMessage(messages.alreadyInstalled)
return null
const serverInstallLoading = computed(
() => !!data.value && installingServerProjects.value.includes(data.value.id),
)
const installButtonIcon = computed(() => {
if (installButtonLoading.value && !installButtonInstalled.value) return SpinnerIcon
if (!installButtonInstalled.value && !serverProjectSelected.value) return DownloadIcon
return CheckIcon
})
const installButtonIconClass = computed(() =>
installButtonLoading.value && !installButtonInstalled.value ? 'animate-spin' : undefined,
)
const serverProjectHeaderMoreActions = computed(() => [
{
id: 'open-in-browser',
label: formatMessage(commonMessages.openInModrinthButton),
icon: ExternalIcon,
action: openProjectInBrowser,
},
{
divider: true,
},
{
id: 'report',
label: formatMessage(commonMessages.reportButton),
icon: ReportIcon,
color: 'red',
action: reportProject,
},
])
const projectHeaderMoreActions = computed(() => [
{
id: 'follow',
label: formatMessage(commonMessages.followButton),
icon: HeartIcon,
disabled: true,
tooltip: 'Coming soon',
action: () => {},
},
{
id: 'save',
label: formatMessage(commonMessages.saveButton),
icon: BookmarkIcon,
disabled: true,
tooltip: 'Coming soon',
action: () => {},
},
{
id: 'open-in-browser',
label: formatMessage(commonMessages.openInModrinthButton),
icon: ExternalIcon,
action: openProjectInBrowser,
},
{
divider: true,
},
{
id: 'report',
label: formatMessage(commonMessages.reportButton),
icon: ReportIcon,
color: 'red',
action: reportProject,
},
])
const projectSearchUrl = computed(
() => `/browse/${isServerProject.value ? 'server' : data.value?.project_type}`,
)
const showSwitchVersion = computed(() => !!instance.value && installed.value)
const onVersionsPage = computed(() => route.name === 'Versions')
@@ -585,6 +598,17 @@ function handleAddServerToInstance() {
showAddServerToInstanceModal(data.value.title, address)
}
function openProjectInBrowser() {
if (!data.value) return
const type = isServerProject.value ? 'project' : data.value.project_type
void openUrl(`https://modrinth.com/${type}/${data.value.slug}`)
}
function reportProject() {
if (!data.value) return
void openUrl(`https://modrinth.com/report?item=project&itemID=${data.value.id}`)
}
async function fetchProjectData() {
const [project, projectV3Result] = await Promise.all([
get_project(route.params.id, 'must_revalidate').catch(handleError),