mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
431 lines
10 KiB
Vue
431 lines
10 KiB
Vue
<template>
|
|
<div class="w-full flex flex-col gap-4" :class="{ 'mt-4': isNuxt }">
|
|
<PageHeader
|
|
:header="props.server?.name || 'Server'"
|
|
:leading="leadingItems"
|
|
:metadata="metadataItems"
|
|
:actions="headerActions"
|
|
:header-class="props.headerClass"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Archon } from '@modrinth/api-client'
|
|
import { NuxtModrinthClient } from '@modrinth/api-client'
|
|
import {
|
|
GlobeIcon,
|
|
LinkIcon,
|
|
LoaderCircleIcon,
|
|
PlayIcon,
|
|
SettingsIcon,
|
|
SlashIcon,
|
|
StopCircleIcon,
|
|
TimerIcon,
|
|
UpdatedIcon,
|
|
} from '@modrinth/assets'
|
|
import { useStorage } from '@vueuse/core'
|
|
import type { Component } from 'vue'
|
|
import { computed } from 'vue'
|
|
import type { RouteLocationRaw } from 'vue-router'
|
|
|
|
import Avatar from '#ui/components/base/Avatar.vue'
|
|
import PageHeader from '#ui/components/base/PageHeader.vue'
|
|
import type { JoinedButtonAction } from '#ui/components/base/JoinedButtons.vue'
|
|
import ServerIcon from '#ui/components/servers/icons/ServerIcon.vue'
|
|
import LoaderIcon from '#ui/components/servers/icons/LoaderIcon.vue'
|
|
import {
|
|
injectModrinthClient,
|
|
injectModrinthServerContext,
|
|
injectNotificationManager,
|
|
} from '#ui/providers'
|
|
import { formatLoaderLabel } from '#ui/utils/loaders'
|
|
|
|
import { useServerPowerAction } from './use-server-power-action'
|
|
|
|
type ServerProjectSummary = {
|
|
id: string
|
|
slug?: string | null
|
|
title: string
|
|
icon_url?: string | null
|
|
}
|
|
|
|
type HeaderWorld = {
|
|
id: string
|
|
name: string
|
|
}
|
|
|
|
type HeaderMetadataItem = {
|
|
id: string
|
|
label: string
|
|
icon: Component
|
|
iconProps?: Record<string, unknown>
|
|
tooltip?: string
|
|
ariaLabel?: string
|
|
to?: string | RouteLocationRaw
|
|
onClick?: () => void | Promise<void>
|
|
class?: string
|
|
}
|
|
|
|
type HeaderAction = {
|
|
id: string
|
|
label: string
|
|
icon?: Component
|
|
iconProps?: Record<string, unknown>
|
|
iconClass?: string
|
|
tooltip?: string
|
|
ariaLabel?: string
|
|
to?: string | RouteLocationRaw
|
|
onClick?: () => void | Promise<void>
|
|
disabled?: boolean
|
|
labelHidden?: boolean
|
|
circular?: boolean
|
|
color?: 'standard' | 'brand' | 'red' | 'orange' | 'green' | 'blue' | 'purple'
|
|
size?: 'standard' | 'large' | 'small'
|
|
type?: 'standard' | 'outlined' | 'transparent' | 'highlight' | 'highlight-colored-text' | 'chip'
|
|
joinedActions?: JoinedButtonAction[]
|
|
primaryDisabled?: boolean
|
|
dropdownDisabled?: boolean
|
|
primaryMuted?: boolean
|
|
prompt?: {
|
|
title: string
|
|
description: string
|
|
dismissLabel?: string
|
|
shown?: boolean
|
|
placement?: string
|
|
onDismiss?: () => void
|
|
}
|
|
}
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
server: Archon.Servers.v0.Server | null | undefined
|
|
serverImage?: string | null
|
|
serverProject?: ServerProjectSummary | null
|
|
serverProjectLink?: string
|
|
activeWorldName?: string | null
|
|
uptimeSeconds?: number
|
|
showUptime?: boolean
|
|
backHref?: string
|
|
breadcrumbClass?: string
|
|
headerClass?: string
|
|
worlds?: HeaderWorld[]
|
|
powerDisabled?: boolean
|
|
settingsLabel?: string
|
|
showSettingsHint?: boolean
|
|
settingsHintTitle?: string
|
|
settingsHintDescription?: string
|
|
settingsHintDismissLabel?: string
|
|
actions?: HeaderAction[]
|
|
}>(),
|
|
{
|
|
serverImage: null,
|
|
serverProject: null,
|
|
serverProjectLink: '',
|
|
activeWorldName: null,
|
|
uptimeSeconds: 0,
|
|
showUptime: true,
|
|
backHref: '/hosting/manage',
|
|
breadcrumbClass: 'breadcrumb goto-link flex w-fit items-center',
|
|
headerClass: '',
|
|
worlds: () => [],
|
|
powerDisabled: false,
|
|
settingsLabel: 'Server settings',
|
|
showSettingsHint: false,
|
|
settingsHintTitle: '',
|
|
settingsHintDescription: '',
|
|
settingsHintDismissLabel: "Don't show again",
|
|
actions: () => [],
|
|
},
|
|
)
|
|
|
|
const emit = defineEmits<{
|
|
openSettings: []
|
|
dismissSettingsHint: []
|
|
}>()
|
|
|
|
const client = injectModrinthClient()
|
|
const { addNotification } = injectNotificationManager()
|
|
const { serverId } = injectModrinthServerContext()
|
|
const isNuxt = computed(() => client instanceof NuxtModrinthClient)
|
|
const {
|
|
isInstalling,
|
|
isStopping,
|
|
showRestartButton,
|
|
busyTooltip,
|
|
canTakeAction,
|
|
canKill,
|
|
primaryActionText,
|
|
initiateAction,
|
|
handlePrimaryAction,
|
|
} = useServerPowerAction({
|
|
disabled: computed(() => props.powerDisabled),
|
|
})
|
|
|
|
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
|
hideSubdomainLabel: false,
|
|
})
|
|
|
|
const headerImage = computed(() => {
|
|
if (props.server?.is_medal) {
|
|
return 'https://cdn-raw.modrinth.com/medal_icon.webp'
|
|
}
|
|
return props.serverImage ?? undefined
|
|
})
|
|
|
|
const leadingItems = computed(() => [
|
|
{
|
|
id: 'server-icon',
|
|
type: 'component' as const,
|
|
component: ServerIcon,
|
|
componentProps: {
|
|
image: headerImage.value,
|
|
},
|
|
class: isNuxt.value ? 'size-15 !rounded-2xl' : 'size-15 !rounded-xl',
|
|
},
|
|
])
|
|
|
|
const showUptime = computed(() => props.showUptime && (props.uptimeSeconds ?? 0) > 0)
|
|
|
|
const formattedUptime = computed(() => {
|
|
const uptime = props.uptimeSeconds ?? 0
|
|
const days = Math.floor(uptime / (24 * 3600))
|
|
const hours = Math.floor((uptime % (24 * 3600)) / 3600)
|
|
const minutes = Math.floor((uptime % 3600) / 60)
|
|
const seconds = uptime % 60
|
|
|
|
let formatted = ''
|
|
if (days > 0) formatted += `${days}d `
|
|
if (hours > 0 || days > 0) formatted += `${hours}h `
|
|
formatted += `${minutes}m ${seconds}s`
|
|
return formatted.trim()
|
|
})
|
|
|
|
const serverAddress = computed(() => {
|
|
const domain = props.server?.net?.domain
|
|
if (domain) return `${domain}.modrinth.gg`
|
|
|
|
const ip = props.server?.net?.ip
|
|
if (!ip) return null
|
|
const port = props.server?.net?.port
|
|
return port ? `${ip}:${port}` : ip
|
|
})
|
|
|
|
const showAddress = computed(
|
|
() => !!serverAddress.value && (!props.server?.net?.domain || !userPreferences.value.hideSubdomainLabel),
|
|
)
|
|
|
|
const metadataItems = computed<HeaderMetadataItem[]>(() => {
|
|
if (props.server?.flows?.intro) {
|
|
return [
|
|
{
|
|
id: 'configuring',
|
|
label: 'Configuring server...',
|
|
icon: SettingsIcon,
|
|
},
|
|
]
|
|
}
|
|
|
|
const items: HeaderMetadataItem[] = []
|
|
const worldName = props.activeWorldName
|
|
if (worldName) {
|
|
items.push({
|
|
id: 'world',
|
|
label: worldName,
|
|
icon: GlobeIcon,
|
|
})
|
|
}
|
|
if (props.server?.loader) {
|
|
items.push({
|
|
id: 'loader',
|
|
label: props.server.mc_version
|
|
? `${formatLoaderLabel(props.server.loader)} ${props.server.mc_version}`
|
|
: formatLoaderLabel(props.server.loader),
|
|
icon: LoaderIcon,
|
|
iconProps: {
|
|
loader: props.server.loader,
|
|
},
|
|
})
|
|
}
|
|
if (showAddress.value && serverAddress.value) {
|
|
items.push({
|
|
id: 'address',
|
|
label: serverAddress.value,
|
|
icon: LinkIcon,
|
|
tooltip: 'Copy server address',
|
|
onClick: copyServerAddress,
|
|
})
|
|
}
|
|
if (showUptime.value) {
|
|
items.push({
|
|
id: 'uptime',
|
|
label: formattedUptime.value,
|
|
icon: TimerIcon,
|
|
})
|
|
}
|
|
if (props.serverProject) {
|
|
items.push({
|
|
id: 'project',
|
|
label: `Linked to ${props.serverProject.title}`,
|
|
icon: Avatar,
|
|
iconProps: {
|
|
src: props.serverProject.icon_url ?? undefined,
|
|
alt: props.serverProject.title,
|
|
size: '24px',
|
|
},
|
|
to: serverProjectLink.value,
|
|
class: 'text-primary',
|
|
})
|
|
}
|
|
return items
|
|
})
|
|
|
|
const serverProjectLink = computed(() => {
|
|
if (props.serverProjectLink) {
|
|
return props.serverProjectLink
|
|
}
|
|
if (!props.serverProject) {
|
|
return ''
|
|
}
|
|
return `/project/${props.serverProject.slug ?? props.serverProject.id}`
|
|
})
|
|
|
|
const startActionText = computed(() =>
|
|
primaryActionText.value === 'Start' ? 'Start server' : primaryActionText.value,
|
|
)
|
|
|
|
const startableWorlds = computed(() => {
|
|
if (props.worlds.length > 0) return props.worlds
|
|
|
|
return [
|
|
{
|
|
id: 'current-world',
|
|
name: props.activeWorldName ?? 'current instance',
|
|
},
|
|
]
|
|
})
|
|
|
|
const startSplitActions = computed<JoinedButtonAction[]>(() => [
|
|
{
|
|
id: 'start',
|
|
label: startActionText.value,
|
|
icon: PlayIcon,
|
|
action: handlePrimaryAction,
|
|
},
|
|
...startableWorlds.value.map((world) => ({
|
|
id: `start-${world.id}`,
|
|
label: `Start with ${world.name}`,
|
|
icon: GlobeIcon,
|
|
action: handlePrimaryAction,
|
|
})),
|
|
])
|
|
|
|
const stopSplitActions = computed<JoinedButtonAction[]>(() => [
|
|
{
|
|
id: 'stop',
|
|
label: isStopping.value ? 'Stopping' : 'Stop',
|
|
icon: StopCircleIcon,
|
|
action: () => initiateAction('Stop'),
|
|
},
|
|
{
|
|
id: 'kill_server',
|
|
label: 'Kill server',
|
|
icon: SlashIcon,
|
|
action: () => initiateAction('Kill'),
|
|
},
|
|
])
|
|
|
|
const powerActions = computed<HeaderAction[]>(() => {
|
|
if (isInstalling.value) {
|
|
return [
|
|
{
|
|
id: 'installing',
|
|
label: 'Installing...',
|
|
icon: LoaderCircleIcon,
|
|
iconClass: 'animate-spin',
|
|
color: 'brand',
|
|
disabled: true,
|
|
},
|
|
]
|
|
}
|
|
if (showRestartButton.value) {
|
|
return [
|
|
{
|
|
id: 'restart',
|
|
label: primaryActionText.value,
|
|
icon: UpdatedIcon,
|
|
color: 'orange',
|
|
tooltip: busyTooltip.value,
|
|
disabled: !canTakeAction.value,
|
|
onClick: handlePrimaryAction,
|
|
},
|
|
{
|
|
id: 'stop',
|
|
label: 'Stop server',
|
|
color: 'red',
|
|
joinedActions: stopSplitActions.value,
|
|
primaryDisabled: !canTakeAction.value,
|
|
dropdownDisabled: !canKill.value,
|
|
},
|
|
]
|
|
}
|
|
if (isStopping.value) {
|
|
return [
|
|
{
|
|
id: 'stop',
|
|
label: 'Stop server',
|
|
color: 'red',
|
|
joinedActions: stopSplitActions.value,
|
|
primaryDisabled: true,
|
|
dropdownDisabled: !canKill.value,
|
|
primaryMuted: true,
|
|
},
|
|
]
|
|
}
|
|
return [
|
|
{
|
|
id: 'start',
|
|
label: startActionText.value,
|
|
color: 'brand',
|
|
tooltip: busyTooltip.value,
|
|
joinedActions: startSplitActions.value,
|
|
primaryDisabled: !canTakeAction.value,
|
|
dropdownDisabled: !canTakeAction.value,
|
|
},
|
|
]
|
|
})
|
|
|
|
const settingsAction = computed<HeaderAction>(() => ({
|
|
id: 'settings',
|
|
label: props.settingsLabel,
|
|
icon: SettingsIcon,
|
|
labelHidden: true,
|
|
tooltip: props.showSettingsHint ? undefined : props.settingsLabel,
|
|
onClick: () => emit('openSettings'),
|
|
prompt: {
|
|
title: props.settingsHintTitle,
|
|
description: props.settingsHintDescription,
|
|
dismissLabel: props.settingsHintDismissLabel,
|
|
shown: props.showSettingsHint,
|
|
placement: 'bottom-end',
|
|
onDismiss: () => emit('dismissSettingsHint'),
|
|
},
|
|
}))
|
|
|
|
const headerActions = computed<HeaderAction[]>(() => [
|
|
...powerActions.value,
|
|
settingsAction.value,
|
|
...props.actions,
|
|
])
|
|
|
|
function copyServerAddress() {
|
|
if (!serverAddress.value) return
|
|
navigator.clipboard.writeText(serverAddress.value)
|
|
addNotification({
|
|
title: 'Server address copied',
|
|
text: "Your server's address has been copied to your clipboard.",
|
|
type: 'success',
|
|
})
|
|
}
|
|
</script>
|