mirror of
https://github.com/modrinth/code.git
synced 2026-09-04 05:48:57 +00:00
fix: header issues
This commit is contained in:
@@ -28,18 +28,31 @@
|
|||||||
<template #stats>
|
<template #stats>
|
||||||
<div class="flex items-center flex-wrap gap-2">
|
<div class="flex items-center flex-wrap gap-2">
|
||||||
<template v-if="!isServerInstance">
|
<template v-if="!isServerInstance">
|
||||||
<div class="flex items-center gap-2 capitalize font-medium">
|
<div class="flex min-w-0 items-center gap-2 font-medium text-secondary text-nowrap">
|
||||||
{{ instance.loader }} {{ instance.game_version }}
|
<Gamepad2Icon class="flex size-5 shrink-0" aria-hidden="true" />
|
||||||
|
<span class="truncate">{{ instance.game_version }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BulletDivider />
|
||||||
|
|
||||||
|
<div class="flex min-w-0 items-center gap-2 font-medium text-secondary text-nowrap">
|
||||||
|
<ServerLoaderIcon
|
||||||
|
v-if="loaderDisplayName"
|
||||||
|
:loader="loaderDisplayName"
|
||||||
|
class="flex size-5 shrink-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span class="truncate">{{ loaderLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="showInstancePlayTime">
|
<template v-if="showInstancePlayTime">
|
||||||
<div class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
|
<BulletDivider />
|
||||||
|
|
||||||
<div class="flex items-center gap-2 font-medium">
|
<div class="flex min-w-0 items-center gap-2 font-medium text-secondary text-nowrap">
|
||||||
<template v-if="timePlayed > 0">
|
<TimerIcon class="flex size-5 shrink-0" aria-hidden="true" />
|
||||||
{{ timePlayedHumanized }}
|
<span class="truncate">
|
||||||
</template>
|
{{ playtimeLabel }}
|
||||||
<template v-else> Never played </template>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -287,16 +300,21 @@ import {
|
|||||||
ServerIcon,
|
ServerIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
|
TagCategoryGamepad2Icon as Gamepad2Icon,
|
||||||
TerminalSquareIcon,
|
TerminalSquareIcon,
|
||||||
|
TimerIcon,
|
||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
BulletDivider,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
ContentPageHeader,
|
ContentPageHeader,
|
||||||
|
formatLoaderLabel,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
|
LoaderIcon as ServerLoaderIcon,
|
||||||
NavTabs,
|
NavTabs,
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
ServerOnlinePlayers,
|
ServerOnlinePlayers,
|
||||||
@@ -305,6 +323,7 @@ import {
|
|||||||
ServerRegion,
|
ServerRegion,
|
||||||
useLoadingBarToken,
|
useLoadingBarToken,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
|
import type { Loaders } from '@modrinth/utils'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
@@ -725,6 +744,16 @@ const timePlayed = computed(() => {
|
|||||||
: 0
|
: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const loaderDisplayName = computed(() =>
|
||||||
|
instance.value ? (formatLoaderLabel(instance.value.loader) as Loaders) : null,
|
||||||
|
)
|
||||||
|
|
||||||
|
const loaderLabel = computed(() =>
|
||||||
|
instance.value && loaderDisplayName.value
|
||||||
|
? [loaderDisplayName.value, instance.value.loader_version].filter(Boolean).join(' ')
|
||||||
|
: '',
|
||||||
|
)
|
||||||
|
|
||||||
const timePlayedHumanized = computed(() => {
|
const timePlayedHumanized = computed(() => {
|
||||||
const duration = dayjs.duration(timePlayed.value, 'seconds')
|
const duration = dayjs.duration(timePlayed.value, 'seconds')
|
||||||
const hours = Math.floor(duration.asHours())
|
const hours = Math.floor(duration.asHours())
|
||||||
@@ -741,6 +770,10 @@ const timePlayedHumanized = computed(() => {
|
|||||||
return seconds + ' second' + (seconds > 1 ? 's' : '')
|
return seconds + ' second' + (seconds > 1 ? 's' : '')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const playtimeLabel = computed(() =>
|
||||||
|
timePlayed.value > 0 ? timePlayedHumanized.value : 'Never played',
|
||||||
|
)
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlistenProcesses()
|
unlistenProcesses()
|
||||||
unlistenInstances()
|
unlistenInstances()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<template #icon>
|
<template #icon>
|
||||||
<ServerIcon
|
<ServerIcon
|
||||||
:image="headerImage"
|
:image="headerImage"
|
||||||
:class="isNuxt ? 'size-20 !rounded-2xl' : 'size-16 !rounded-xl'"
|
:class="isNuxt ? 'size-15 !rounded-2xl' : 'size-15 !rounded-xl'"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #title>
|
<template #title>
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full flex flex-col gap-4" :class="{ 'mt-4': isNuxt }">
|
||||||
|
<ContentPageHeader :class="props.headerClass">
|
||||||
|
<template #icon>
|
||||||
|
<div class="flex size-16 shrink-0 items-center justify-center">
|
||||||
|
<ButtonStyled circular size="large">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:aria-label="props.backLabel"
|
||||||
|
@click="router.push(props.backHref)"
|
||||||
|
>
|
||||||
|
<LeftArrowIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
{{ props.name || props.fallbackName }}
|
||||||
|
</template>
|
||||||
|
<template #stats>
|
||||||
|
<div v-if="headerMetadata.length" class="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
|
<template v-for="(item, index) in headerMetadata" :key="item.id">
|
||||||
|
<BulletDivider v-if="index > 0" />
|
||||||
|
<div class="flex min-w-0 items-center gap-2 font-medium text-secondary text-nowrap">
|
||||||
|
<component
|
||||||
|
:is="item.icon"
|
||||||
|
class="flex size-5 shrink-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
v-bind="item.iconProps"
|
||||||
|
/>
|
||||||
|
<span class="truncate">{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<slot name="actions" />
|
||||||
|
</template>
|
||||||
|
</ContentPageHeader>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NuxtModrinthClient } from '@modrinth/api-client'
|
||||||
|
import { LeftArrowIcon, TagCategoryGamepad2Icon as Gamepad2Icon, TimerIcon } from '@modrinth/assets'
|
||||||
|
import { type Component, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import BulletDivider from '#ui/components/base/BulletDivider.vue'
|
||||||
|
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||||
|
import ContentPageHeader from '#ui/components/base/ContentPageHeader.vue'
|
||||||
|
import LoaderIcon from '#ui/components/servers/icons/LoaderIcon.vue'
|
||||||
|
import { injectModrinthClient } from '#ui/providers'
|
||||||
|
import { formatLoaderLabel } from '#ui/utils/loaders'
|
||||||
|
|
||||||
|
type MetadataItem = {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
icon: Component
|
||||||
|
iconProps?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
name?: string | null
|
||||||
|
metadataItems?: MetadataItem[]
|
||||||
|
gameVersion?: string | null
|
||||||
|
loader?: string | null
|
||||||
|
loaderVersion?: string | null
|
||||||
|
lastActive?: string | null
|
||||||
|
backHref: string
|
||||||
|
backLabel: string
|
||||||
|
fallbackName?: string
|
||||||
|
headerClass?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
name: null,
|
||||||
|
metadataItems: () => [],
|
||||||
|
gameVersion: null,
|
||||||
|
loader: null,
|
||||||
|
loaderVersion: null,
|
||||||
|
lastActive: null,
|
||||||
|
fallbackName: 'World',
|
||||||
|
headerClass: '',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const client = injectModrinthClient()
|
||||||
|
const router = useRouter()
|
||||||
|
const isNuxt = computed(() => client instanceof NuxtModrinthClient)
|
||||||
|
const loaderLabel = computed(() => {
|
||||||
|
if (!props.loader) return null
|
||||||
|
const label = formatLoaderLabel(props.loader.toLowerCase())
|
||||||
|
return [label, props.loaderVersion].filter(Boolean).join(' ')
|
||||||
|
})
|
||||||
|
const headerMetadata = computed<MetadataItem[]>(() => {
|
||||||
|
if (props.metadataItems.length) return props.metadataItems
|
||||||
|
|
||||||
|
const items: MetadataItem[] = []
|
||||||
|
if (props.gameVersion) {
|
||||||
|
items.push({
|
||||||
|
id: 'game-version',
|
||||||
|
label: props.gameVersion,
|
||||||
|
icon: Gamepad2Icon,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (props.loader && loaderLabel.value) {
|
||||||
|
items.push({
|
||||||
|
id: 'loader',
|
||||||
|
label: loaderLabel.value,
|
||||||
|
icon: LoaderIcon,
|
||||||
|
iconProps: {
|
||||||
|
loader: formatLoaderLabel(props.loader.toLowerCase()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (props.lastActive) {
|
||||||
|
items.push({
|
||||||
|
id: 'last-active',
|
||||||
|
label: props.lastActive,
|
||||||
|
icon: TimerIcon,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export { default as PanelServerActionButton } from './PanelServerActionButton.vue'
|
export { default as PanelServerActionButton } from './PanelServerActionButton.vue'
|
||||||
export { default as ServerManageHeader } from './ServerManageHeader.vue'
|
export { default as ServerManageHeader } from './ServerManageHeader.vue'
|
||||||
|
export { default as WorldManageHeader } from './WorldManageHeader.vue'
|
||||||
|
|||||||
+36
-65
@@ -1,42 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex min-h-[36rem] flex-col gap-6 text-primary">
|
<div class="flex min-h-[36rem] flex-col gap-4 text-primary">
|
||||||
<div class="flex flex-col gap-2">
|
<WorldManageHeader
|
||||||
<RouterLink
|
:name="worldName"
|
||||||
:to="instancesPath"
|
:game-version="gameVersion"
|
||||||
class="flex w-fit items-center gap-1 text-base font-medium text-blue hover:underline"
|
:loader="loader"
|
||||||
>
|
:loader-version="loaderVersion"
|
||||||
<ChevronLeftIcon class="size-4" aria-hidden="true" />
|
:last-active="lastActiveLabel"
|
||||||
{{ formatMessage(messages.allInstances) }}
|
:back-href="instancesPath"
|
||||||
</RouterLink>
|
:back-label="formatMessage(messages.allInstances)"
|
||||||
|
:fallback-name="formatMessage(messages.worldFallbackName)"
|
||||||
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
>
|
||||||
<div class="flex min-w-0 flex-col gap-1">
|
<template #actions>
|
||||||
<h1 class="m-0 truncate text-2xl font-semibold leading-8 text-contrast">
|
<PanelServerActionButton size="large" start-label="Start instance" />
|
||||||
{{ worldName }}
|
<ButtonStyled size="large" circular>
|
||||||
</h1>
|
<button
|
||||||
<div class="flex flex-wrap items-center gap-2 text-base font-medium text-secondary">
|
v-tooltip="formatMessage(messages.instanceSettings)"
|
||||||
<template v-for="(item, index) in worldMetadata" :key="item">
|
@click="openServerSettings({ tabId: 'installation' })"
|
||||||
<span>{{ item }}</span>
|
>
|
||||||
<BulletDivider v-if="index < worldMetadata.length - 1" />
|
<SettingsIcon aria-hidden="true" />
|
||||||
</template>
|
</button>
|
||||||
</div>
|
</ButtonStyled>
|
||||||
</div>
|
</template>
|
||||||
|
</WorldManageHeader>
|
||||||
<div class="flex shrink-0 items-center gap-2">
|
|
||||||
<PanelServerActionButton size="large" start-label="Start instance" />
|
|
||||||
<ButtonStyled size="large" circular>
|
|
||||||
<button
|
|
||||||
v-tooltip="formatMessage(messages.instanceSettings)"
|
|
||||||
@click="openServerSettings({ tabId: 'installation' })"
|
|
||||||
>
|
|
||||||
<SettingsIcon aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="h-px w-full bg-surface-5" />
|
|
||||||
|
|
||||||
<NavTabs :links="worldTabLinks" replace />
|
<NavTabs :links="worldTabLinks" replace />
|
||||||
|
|
||||||
@@ -46,21 +31,14 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Archon } from '@modrinth/api-client'
|
import type { Archon } from '@modrinth/api-client'
|
||||||
import {
|
import { BoxesIcon, DatabaseBackupIcon, FolderOpenIcon, SettingsIcon } from '@modrinth/assets'
|
||||||
BoxesIcon,
|
|
||||||
ChevronLeftIcon,
|
|
||||||
DatabaseBackupIcon,
|
|
||||||
FolderOpenIcon,
|
|
||||||
SettingsIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch } from 'vue'
|
||||||
import { RouterLink, useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import BulletDivider from '#ui/components/base/BulletDivider.vue'
|
|
||||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||||
import NavTabs from '#ui/components/base/NavTabs.vue'
|
import NavTabs from '#ui/components/base/NavTabs.vue'
|
||||||
import { PanelServerActionButton } from '#ui/components/servers/server-header'
|
import { PanelServerActionButton, WorldManageHeader } from '#ui/components/servers/server-header'
|
||||||
import { useRelativeTime } from '#ui/composables'
|
import { useRelativeTime } from '#ui/composables'
|
||||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||||
import {
|
import {
|
||||||
@@ -68,7 +46,6 @@ import {
|
|||||||
injectModrinthServerContext,
|
injectModrinthServerContext,
|
||||||
injectServerSettingsModal,
|
injectServerSettingsModal,
|
||||||
} from '#ui/providers'
|
} from '#ui/providers'
|
||||||
import { formatLoaderLabel } from '#ui/utils/loaders'
|
|
||||||
|
|
||||||
interface Tab {
|
interface Tab {
|
||||||
label: string
|
label: string
|
||||||
@@ -138,24 +115,18 @@ const worldName = computed(
|
|||||||
() => currentWorld.value?.name ?? server.value?.name ?? formatMessage(messages.worldFallbackName),
|
() => currentWorld.value?.name ?? server.value?.name ?? formatMessage(messages.worldFallbackName),
|
||||||
)
|
)
|
||||||
|
|
||||||
const worldMetadata = computed(() =>
|
const gameVersion = computed(() => {
|
||||||
[gameVersionLabel.value, loaderLabel.value, lastActiveLabel.value].filter(
|
const version = currentWorld.value?.content?.game_version ?? server.value?.mc_version
|
||||||
(item): item is string => !!item,
|
return version ?? null
|
||||||
),
|
})
|
||||||
|
|
||||||
|
const loader = computed(
|
||||||
|
() => currentWorld.value?.content?.modloader ?? server.value?.loader ?? null,
|
||||||
)
|
)
|
||||||
|
|
||||||
const gameVersionLabel = computed(() => {
|
const loaderVersion = computed(
|
||||||
const version = currentWorld.value?.content?.game_version ?? server.value?.mc_version
|
() => currentWorld.value?.content?.modloader_version ?? server.value?.loader_version ?? null,
|
||||||
return version ? `MC ${version}` : null
|
)
|
||||||
})
|
|
||||||
|
|
||||||
const loaderLabel = computed(() => {
|
|
||||||
const loader = currentWorld.value?.content?.modloader ?? server.value?.loader?.toLowerCase()
|
|
||||||
if (!loader) return null
|
|
||||||
const loaderVersion =
|
|
||||||
currentWorld.value?.content?.modloader_version ?? server.value?.loader_version ?? null
|
|
||||||
return [formatLoaderLabel(loader), loaderVersion].filter(Boolean).join(' ')
|
|
||||||
})
|
|
||||||
|
|
||||||
const lastActiveLabel = computed(() => {
|
const lastActiveLabel = computed(() => {
|
||||||
const latestBackup = currentWorld.value ? latestDate(currentWorld.value.backups) : null
|
const latestBackup = currentWorld.value ? latestDate(currentWorld.value.backups) : null
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { SettingsIcon } from '@modrinth/assets'
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
|
||||||
|
import ButtonStyled from '../../components/base/ButtonStyled.vue'
|
||||||
|
import WorldManageHeader from '../../components/servers/server-header/WorldManageHeader.vue'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Servers/WorldManageHeader',
|
||||||
|
component: WorldManageHeader,
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded',
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(story) => ({
|
||||||
|
components: { story },
|
||||||
|
template: '<div style="max-width: 920px;"><story /></div>',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} satisfies Meta<typeof WorldManageHeader>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
name: 'My World',
|
||||||
|
gameVersion: '1.20.1',
|
||||||
|
loader: 'fabric',
|
||||||
|
loaderVersion: '0.19.2',
|
||||||
|
lastActive: 'Last active 2 weeks ago',
|
||||||
|
backHref: '/hosting/manage/demo-server/instances',
|
||||||
|
backLabel: 'All instances',
|
||||||
|
},
|
||||||
|
render: (args) => ({
|
||||||
|
components: { ButtonStyled, SettingsIcon, WorldManageHeader },
|
||||||
|
setup() {
|
||||||
|
return { args }
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<WorldManageHeader v-bind="args">
|
||||||
|
<template #actions>
|
||||||
|
<ButtonStyled size="large" circular>
|
||||||
|
<button type="button" aria-label="Instance settings">
|
||||||
|
<SettingsIcon aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
</WorldManageHeader>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user