mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 13:16:38 +00:00
* fix: 20 user cap * qa: invite modal dropdown + other + clamp limits * fix: hide filters if just one type * feat: update req banner * feat: invite management frontend * fix: moderation issues with shared instances * feat: instance icon improvements * fix: better offline handling * fix: lint * feat: show avatar in install to play modal * feat: smaller qa points * fix: fmt * fix: fmt * fix: yeet svg * fix: 50 user limit
410 lines
11 KiB
Vue
410 lines
11 KiB
Vue
<template>
|
|
<PageHeader :title="instance.name">
|
|
<template #leading>
|
|
<Avatar :src="iconSrc" :alt="instance.name" size="64px" :tint-by="instance.id" />
|
|
</template>
|
|
|
|
<template v-if="instance.shared_instance || instance.quarantined" #badges>
|
|
<PageHeaderBadgeItem
|
|
v-if="instance.quarantined"
|
|
:icon="LockIcon"
|
|
aria-label="Locked instance information"
|
|
class="!border-orange !bg-highlight-orange !text-orange"
|
|
>
|
|
Locked
|
|
</PageHeaderBadgeItem>
|
|
<PageHeaderBadgeItem
|
|
v-else
|
|
:tooltip="sharedInstanceTooltip"
|
|
aria-label="Shared instance information"
|
|
class="!border-blue !bg-highlight-blue !text-blue"
|
|
>
|
|
Shared
|
|
<UnknownIcon class="block size-4 shrink-0 text-current" aria-hidden="true" />
|
|
</PageHeaderBadgeItem>
|
|
</template>
|
|
|
|
<template #metadata>
|
|
<div v-if="isServerInstance" class="flex flex-wrap items-center gap-2">
|
|
<InstanceHeaderServerMetadata
|
|
: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"
|
|
/>
|
|
<PageHeaderMetadataItem v-if="sharedInstanceManager" :action="sharedInstanceManagerAction">
|
|
{{ sharedInstanceManagerLabel }}
|
|
<Avatar
|
|
:src="sharedInstanceManager.avatarUrl"
|
|
:alt="sharedInstanceManager.name"
|
|
:tint-by="sharedInstanceManager.tintBy"
|
|
size="24px"
|
|
:circle="sharedInstanceManager.type === 'user'"
|
|
no-shadow
|
|
/>
|
|
<span class="min-w-0 truncate">{{ sharedInstanceManager.name }}</span>
|
|
</PageHeaderMetadataItem>
|
|
</div>
|
|
<PageHeaderMetadata v-else>
|
|
<PageHeaderMetadataItem :icon="Gamepad2Icon" tooltip="Minecraft version">
|
|
Minecraft {{ instance.game_version }}
|
|
</PageHeaderMetadataItem>
|
|
<PageHeaderMetadataItem
|
|
v-if="sharedInstanceManager?.type !== 'user'"
|
|
:icon="ServerLoaderIcon"
|
|
:icon-props="{ loader: loaderDisplayName }"
|
|
tooltip="Mod loader"
|
|
>
|
|
{{ loaderLabel }}
|
|
</PageHeaderMetadataItem>
|
|
<PageHeaderMetadataItem
|
|
v-if="showInstancePlayTime"
|
|
:icon="TimerIcon"
|
|
tooltip="Total playtime"
|
|
>
|
|
{{ playtimeLabel }}
|
|
</PageHeaderMetadataItem>
|
|
<PageHeaderMetadataItem v-if="sharedInstanceManager" :action="sharedInstanceManagerAction">
|
|
{{ sharedInstanceManagerLabel }}
|
|
<Avatar
|
|
:src="sharedInstanceManager.avatarUrl"
|
|
:alt="sharedInstanceManager.name"
|
|
:tint-by="sharedInstanceManager.tintBy"
|
|
size="24px"
|
|
:circle="sharedInstanceManager.type === 'user'"
|
|
no-shadow
|
|
/>
|
|
<span class="min-w-0 truncate">{{ sharedInstanceManager.name }}</span>
|
|
</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="playing" color="red" size="large">
|
|
<button type="button" :disabled="stopping" @click="emit('stop')">
|
|
<StopCircleIcon />
|
|
{{
|
|
stopping ? formatMessage(messages.stopping) : formatMessage(commonMessages.stopButton)
|
|
}}
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled v-else-if="instance.quarantined" color="brand" size="large">
|
|
<button v-tooltip="formatMessage(messages.lockedPlayTooltip)" type="button" disabled>
|
|
<PlayIcon />
|
|
{{ formatMessage(commonMessages.playButton) }}
|
|
</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>
|
|
<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,
|
|
LockIcon,
|
|
MoreVerticalIcon,
|
|
PackageIcon,
|
|
PlayIcon,
|
|
ReportIcon,
|
|
SettingsIcon,
|
|
StopCircleIcon,
|
|
TagCategoryGamepad2Icon as Gamepad2Icon,
|
|
TimerIcon,
|
|
UnknownIcon,
|
|
} from '@modrinth/assets'
|
|
import {
|
|
Avatar,
|
|
ButtonStyled,
|
|
commonMessages,
|
|
defineMessages,
|
|
formatLoaderLabel,
|
|
type JoinedButtonAction,
|
|
JoinedButtons,
|
|
LoaderIcon as ServerLoaderIcon,
|
|
PageHeader,
|
|
PageHeaderActions,
|
|
PageHeaderBadgeItem,
|
|
PageHeaderMetadata,
|
|
PageHeaderMetadataItem,
|
|
type ServerLoader,
|
|
TeleportOverflowMenu,
|
|
type TeleportOverflowMenuItem,
|
|
useVIntl,
|
|
} from '@modrinth/ui'
|
|
import { computed } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
|
|
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',
|
|
},
|
|
lockedPlayTooltip: {
|
|
id: 'instance.locked.play-tooltip',
|
|
defaultMessage: 'This instance has been locked',
|
|
},
|
|
starting: {
|
|
id: 'instance.action.starting',
|
|
defaultMessage: 'Starting...',
|
|
},
|
|
stopping: {
|
|
id: 'instance.action.stopping',
|
|
defaultMessage: 'Stopping...',
|
|
},
|
|
sharedInstanceTooltip: {
|
|
id: 'instance.shared-instance.tooltip',
|
|
defaultMessage: "This instance's content is being managed by someone else.",
|
|
},
|
|
sharedInstanceOwnerTooltip: {
|
|
id: 'instance.shared-instance.owner-tooltip',
|
|
defaultMessage: "This instance's content is being shared to other users.",
|
|
},
|
|
})
|
|
const router = useRouter()
|
|
|
|
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
|
|
sharedInstanceManager?: {
|
|
type: 'user' | 'server'
|
|
name: string
|
|
avatarUrl?: string
|
|
tintBy: string
|
|
} | null
|
|
}>(),
|
|
{
|
|
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,
|
|
sharedInstanceManager: null,
|
|
},
|
|
)
|
|
|
|
const emit = defineEmits<{
|
|
repair: []
|
|
stop: []
|
|
play: []
|
|
playServer: []
|
|
settings: []
|
|
openFolder: []
|
|
export: []
|
|
createShortcut: []
|
|
report: [event?: MouseEvent]
|
|
}>()
|
|
|
|
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 sharedInstanceTooltip = computed(() =>
|
|
formatMessage(
|
|
props.instance.shared_instance?.role === 'owner'
|
|
? messages.sharedInstanceOwnerTooltip
|
|
: messages.sharedInstanceTooltip,
|
|
),
|
|
)
|
|
const sharedInstanceManagerLabel = computed(() =>
|
|
props.sharedInstanceManager?.type === 'server' ? 'Linked to' : 'Managed by',
|
|
)
|
|
const sharedInstanceManagerAction = computed(() => {
|
|
const manager = props.sharedInstanceManager
|
|
if (manager?.type !== 'user') return undefined
|
|
return () => router.push(`/user/${encodeURIComponent(manager.name)}`)
|
|
})
|
|
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[]>(() => {
|
|
const actions: TeleportOverflowMenuItem[] = [
|
|
{
|
|
id: 'open-folder',
|
|
label: formatMessage(messages.openFolder),
|
|
icon: FolderOpenIcon,
|
|
action: () => emit('openFolder'),
|
|
},
|
|
]
|
|
|
|
if (!props.instance.quarantined) {
|
|
actions.push(
|
|
{
|
|
id: 'export-mrpack',
|
|
label: formatMessage(messages.exportModpack),
|
|
icon: PackageIcon,
|
|
action: () => emit('export'),
|
|
},
|
|
{
|
|
id: 'create-shortcut',
|
|
label: formatMessage(messages.createShortcut),
|
|
icon: ExternalIcon,
|
|
action: () => emit('createShortcut'),
|
|
},
|
|
)
|
|
}
|
|
|
|
if (props.instance.shared_instance?.role === 'member') {
|
|
actions.push(
|
|
{ divider: true },
|
|
{
|
|
id: 'report-shared-instance',
|
|
label: formatMessage(commonMessages.reportButton),
|
|
icon: ReportIcon,
|
|
color: 'red',
|
|
action: (event) => emit('report', event),
|
|
},
|
|
)
|
|
}
|
|
|
|
return actions
|
|
})
|
|
</script>
|