refactor: content tab styling (#7091)

* refactor: managed content card

* fix: update filter pill styling to be chips-like

* fix: modpack exporting

* fixed: vanilla server projs + header

* refactor: filters

* refactor: detect external file icons + other metadata inside

* refactor: support json5 metadata

* fix: qa

* fix: qa

* fix: fmt + prepr

* fix: fmt

* fix: qa

* fix: headers

* feat: locking

* fix: clippy

* fix: qa

* fix: frozen loc

* fix: shared instances diff calc break

* fix: env filter + remove categories
This commit is contained in:
Calum H.
2026-08-13 14:55:35 +00:00
committed by GitHub
parent e087e571f3
commit 06b7c44dcc
126 changed files with 3654 additions and 3473 deletions
+11 -31
View File
@@ -399,9 +399,10 @@ const hideInstalledModpacks = computed({
const instanceFilters = computed(() => {
const filters = []
if (instance.value) {
if (instance.value && projectType.value !== 'resourcepack') {
const isVanillaShader = projectType.value === 'shader' && instance.value.loader === 'vanilla'
const gameVersion = instance.value.game_version
if (gameVersion) {
if (gameVersion && !isVanillaShader) {
filters.push({ type: 'game_version', option: gameVersion })
}
@@ -411,6 +412,9 @@ const instanceFilters = computed(() => {
if (platform && projectType.value === 'mod' && supportedModLoaders.includes(platform)) {
filters.push({ type: 'mod_loader', option: platform })
}
if (isVanillaShader) {
filters.push({ type: 'shader_loader', option: 'vanilla' })
}
if (isServerInstance.value) {
filters.push({ type: 'environment', option: 'client' })
@@ -550,10 +554,6 @@ const messages = defineMessages({
id: 'search.filter.locked.instance-game-version.title',
defaultMessage: 'Game version is provided by the instance',
},
gameVersionProvidedByServer: {
id: 'search.filter.locked.server-game-version.title',
defaultMessage: 'Game version is provided by the server',
},
hideAddedServers: {
id: 'app.browse.hide-added-servers',
defaultMessage: 'Hide servers already added',
@@ -573,7 +573,7 @@ const messages = defineMessages({
serverInstanceContentWarning: {
id: 'app.browse.server-instance-content-warning',
defaultMessage:
'Adding content can break compatibility when joining the server. Any added content will also be lost when you update the server instance content.',
'Adding content may prevent you from joining this server. Any content you add will be removed when the managed server content is updated.',
},
modLoaderProvidedByInstance: {
id: 'search.filter.locked.instance-loader.title',
@@ -583,18 +583,10 @@ const messages = defineMessages({
id: 'app.browse.project-type.modpacks',
defaultMessage: 'Modpacks',
},
modLoaderProvidedByServer: {
id: 'search.filter.locked.server-loader.title',
defaultMessage: 'Loader is provided by the server',
},
providedByInstance: {
id: 'search.filter.locked.instance',
defaultMessage: 'Provided by the instance',
},
providedByServer: {
id: 'search.filter.locked.server',
defaultMessage: 'Provided by the server',
},
syncFilterButton: {
id: 'search.filter.locked.instance.sync',
defaultMessage: 'Sync with instance',
@@ -743,7 +735,7 @@ const installContext = computed(() => {
isFromWorlds.value ? messages.addServersToInstance : commonMessages.installingContentLabel,
),
warning:
isServerInstance.value && !isFromWorlds.value
isServerInstance.value && instance.value.loader !== 'vanilla' && !isFromWorlds.value
? formatMessage(messages.serverInstanceContentWarning)
: undefined,
}
@@ -1106,24 +1098,12 @@ async function search(requestParams: string) {
}
}
const isServerFilterContext = computed(() => isServerContext.value || isServerInstance.value)
const lockedFilterMessages = computed(() => ({
gameVersion: formatMessage(
isServerFilterContext.value
? messages.gameVersionProvidedByServer
: messages.gameVersionProvidedByInstance,
),
modLoader: formatMessage(
isServerFilterContext.value
? messages.modLoaderProvidedByServer
: messages.modLoaderProvidedByInstance,
),
gameVersion: formatMessage(messages.gameVersionProvidedByInstance),
modLoader: formatMessage(messages.modLoaderProvidedByInstance),
environment: formatMessage(messages.environmentProvidedByServer),
syncButton: formatMessage(messages.syncFilterButton),
providedBy: formatMessage(
isServerFilterContext.value ? messages.providedByServer : messages.providedByInstance,
),
providedBy: formatMessage(messages.providedByInstance),
}))
const searchState = useBrowseSearch({
@@ -6,11 +6,6 @@
:instance="instance"
@published="emit('published')"
/>
<InstanceAdmonitionsSharedInstanceUpdateAvailable
v-else-if="item.kind === 'shared-instance-update-available'"
:instance-name="instance.name"
@review="emit('review-update', $event)"
/>
<InstanceAdmonitionsSharedInstanceWrongAccount
v-else-if="item.kind === 'shared-instance-wrong-account'"
:expected-user-id="sharedInstanceExpectedUserId"
@@ -38,7 +33,6 @@ import type { GameInstance } from '@/helpers/types'
import InstanceAdmonitionsSharedInstanceStale from './shared-instance-stale.vue'
import InstanceAdmonitionsSharedInstanceUnavailable from './shared-instance-unavailable.vue'
import InstanceAdmonitionsSharedInstanceUpdateAvailable from './shared-instance-update-available.vue'
import InstanceAdmonitionsSharedInstanceWrongAccount from './shared-instance-wrong-account.vue'
import type { InstanceAdmonitionItem, SharedInstanceRole } from './types.ts'
@@ -54,13 +48,11 @@ const props = defineProps<{
sharedInstanceExpectedUserId?: string | null
sharedInstanceRole?: SharedInstanceRole | null
sharedInstanceSignedOut?: boolean
sharedInstanceUpdateAvailable?: boolean
}>()
const emit = defineEmits<{
published: []
delete: []
'review-update': [event: MouseEvent]
}>()
const sharedInstanceWrongAccount = computed(() => props.sharedInstanceWrongAccount ?? false)
@@ -76,15 +68,6 @@ const showSharedInstancePublishAdmonition = computed(
props.instance.shared_instance?.role === 'owner' &&
props.instance.shared_instance.status === 'stale',
)
const showSharedInstanceUpdateAdmonition = computed(
() =>
!sharedInstanceWrongAccount.value &&
!displayedSharedInstanceUnavailableReason.value &&
props.instance.install_stage === 'installed' &&
props.sharedInstanceRole === 'member' &&
props.sharedInstanceUpdateAvailable === true,
)
const stackItems = computed<InstanceAdmonitionItem[]>(() => {
const items: InstanceAdmonitionItem[] = []
@@ -120,15 +103,6 @@ const stackItems = computed<InstanceAdmonitionItem[]>(() => {
})
}
if (showSharedInstanceUpdateAdmonition.value) {
items.push({
id: 'shared-instance-update-available',
type: 'info',
dismissible: false,
kind: 'shared-instance-update-available',
})
}
return items
})
@@ -21,19 +21,6 @@ export const instanceAdmonitionsMessages = defineMessages({
id: 'app.instance.admonitions.shared-instance.reviewing-button',
defaultMessage: 'Reviewing...',
},
sharedInstanceUpdateAvailableHeader: {
id: 'app.instance.admonitions.shared-instance.update-available-header',
defaultMessage: 'An update is available',
},
sharedInstanceUpdateAvailableBody: {
id: 'app.instance.admonitions.shared-instance.update-available-body',
defaultMessage:
'An update is required to play {name}. Please update to latest version to launch the game.',
},
sharedInstanceReviewUpdateButton: {
id: 'app.instance.admonitions.shared-instance.review-update-button',
defaultMessage: 'Review update',
},
sharedInstanceReviewHeader: {
id: 'app.instance.admonitions.shared-instance.review-header',
defaultMessage: 'Review changes',
@@ -1,32 +0,0 @@
<template>
<Admonition
type="info"
inline-actions
:header="formatMessage(messages.sharedInstanceUpdateAvailableHeader)"
>
{{ formatMessage(messages.sharedInstanceUpdateAvailableBody, { name: instanceName }) }}
<template #actions>
<Button type="colored" color="blue" size="lg" @click="emit('review', $event)">
<DownloadIcon aria-hidden="true" />
{{ formatMessage(messages.sharedInstanceReviewUpdateButton) }}
</Button>
</template>
</Admonition>
</template>
<script setup lang="ts">
import { DownloadIcon } from '@modrinth/assets'
import { Admonition, Button, useVIntl } from '@modrinth/ui'
import { instanceAdmonitionsMessages as messages } from './messages'
defineProps<{
instanceName: string
}>()
const emit = defineEmits<{
review: [event: MouseEvent]
}>()
const { formatMessage } = useVIntl()
</script>
@@ -2,7 +2,6 @@ import type { StackedAdmonitionItem } from '@modrinth/ui'
export type InstanceAdmonitionKind =
| 'shared-instance-stale'
| 'shared-instance-update-available'
| 'shared-instance-unavailable'
| 'shared-instance-wrong-account'
@@ -25,60 +25,39 @@
</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>
<InstanceHeaderServerMetadata
v-if="isServerInstance"
:loading-server-ping="loadingServerPing"
:players-online="playersOnline"
:status-online="statusOnline"
:ping="ping"
:minecraft-server="minecraftServer"
:show-instance-play-time="showInstancePlayTime"
:playtime-label="playtimeLabel"
/>
<PageHeaderMetadata v-else>
<PageHeaderMetadataItem :icon="Gamepad2Icon" tooltip="Minecraft version">
Minecraft {{ instance.game_version }}
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
v-if="sharedInstanceManager?.type !== 'user'"
:icon="TagIcon"
:icon-props="{ tag: loaderDisplayName, enforceType: 'loader' }"
tooltip="Mod loader"
tooltip="Mod loader and Minecraft version"
>
{{ loaderLabel }}
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
v-if="showInstancePlayTime"
v-if="showInstancePlayTime && playtimeLabel"
: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>
<PageHeaderMetadataTimeItem
v-if="instance.last_played"
:icon="ClockIcon"
:date="instance.last_played"
:label="formatMessage(messages.lastPlayed)"
/>
<PageHeaderMetadataItem v-else :icon="ClockIcon" tooltip="Last played">
{{ formatMessage(messages.neverPlayed) }}
</PageHeaderMetadataItem>
</PageHeaderMetadata>
</template>
@@ -185,6 +164,7 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import {
ClockIcon,
DownloadIcon,
ExternalIcon,
FolderOpenIcon,
@@ -195,7 +175,6 @@ import {
ReportIcon,
SettingsIcon,
StopCircleIcon,
TagCategoryGamepad2Icon as Gamepad2Icon,
TimerIcon,
UnknownIcon,
} from '@modrinth/assets'
@@ -211,12 +190,12 @@ import {
PageHeaderBadgeItem,
PageHeaderMetadata,
PageHeaderMetadataItem,
PageHeaderMetadataTimeItem,
type ServerLoader,
TagIcon,
useVIntl,
} from '@modrinth/ui'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import type { GameInstance } from '@/helpers/types'
@@ -247,6 +226,10 @@ const messages = defineMessages({
id: 'instance.playtime.never-played',
defaultMessage: 'Never played',
},
lastPlayed: {
id: 'instance.last-played',
defaultMessage: 'Last played',
},
openFolder: {
id: 'instance.action.open-folder',
defaultMessage: 'Open folder',
@@ -276,7 +259,6 @@ const messages = defineMessages({
defaultMessage: "This instance's content is being shared to other users.",
},
})
const router = useRouter()
const props = withDefaults(
defineProps<{
@@ -291,16 +273,8 @@ const props = withDefaults(
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,
@@ -313,11 +287,8 @@ const props = withDefaults(
loadingServerPing: false,
playersOnline: undefined,
statusOnline: false,
recentPlays: undefined,
ping: undefined,
minecraftServer: undefined,
linkedProjectV3: undefined,
sharedInstanceManager: null,
},
)
@@ -346,7 +317,7 @@ 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(' '),
[loaderDisplayName.value, props.instance.game_version].filter(Boolean).join(' '),
)
const sharedInstanceTooltip = computed(() =>
formatMessage(
@@ -355,29 +326,23 @@ const sharedInstanceTooltip = computed(() =>
: 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 seconds = Math.floor(props.timePlayed)
if (seconds <= 0) {
return undefined
}
const hours = Math.floor(props.timePlayed / 3600)
const hours = Math.floor(seconds / 3600)
if (hours >= 1) {
return `${hours} hour${hours > 1 ? 's' : ''}`
}
const minutes = Math.floor(props.timePlayed / 60)
const minutes = Math.floor(seconds / 60)
if (minutes >= 1) {
return `${minutes} minute${minutes > 1 ? 's' : ''}`
}
const seconds = Math.floor(props.timePlayed)
return `${seconds} second${seconds > 1 ? 's' : ''}`
return `${seconds} second${seconds === 1 ? '' : 's'}`
})
const serverPlayOptions = computed<OverflowMenuOption[]>(() => [
{
@@ -1,52 +1,36 @@
<template>
<div class="flex items-center flex-wrap gap-2">
<template v-if="loadingServerPing">
<ServerOnlinePlayers
v-if="playersOnline !== undefined"
:online="playersOnline"
<PageHeaderMetadata>
<PageHeaderMetadataItem v-if="loadingServerPing && playersOnline !== undefined">
<ServerOnlinePlayers :online="playersOnline" :status-online="statusOnline" hide-label />
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
v-if="minecraftServer?.region || (loadingServerPing && ping !== undefined)"
>
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer.region" />
<ServerPing
v-if="loadingServerPing && ping !== undefined"
:ping="ping"
: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>
</PageHeaderMetadataItem>
<PageHeaderMetadataItem
v-if="showInstancePlayTime && playtimeLabel"
:icon="TimerIcon"
tooltip="Total playtime"
>
{{ playtimeLabel }}
</PageHeaderMetadataItem>
</PageHeaderMetadata>
</template>
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { TimerIcon } from '@modrinth/assets'
import {
Avatar,
PageHeaderMetadata,
PageHeaderMetadataItem,
ServerOnlinePlayers,
ServerPing,
ServerRecentPlays,
ServerRegion,
} from '@modrinth/ui'
@@ -54,10 +38,9 @@ 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
showInstancePlayTime?: boolean
playtimeLabel?: string
}>()
</script>
@@ -232,6 +232,7 @@ provideInstallationSettings({
() =>
isModrinthLinkedModpack.value ||
isImportedModpack.value ||
instance.value.link?.type === 'server_project' ||
isSharedInstanceManagedModpack.value,
),
isBusy: installationSettingsBusy,
@@ -15,27 +15,28 @@
:share-text="formatMessage(messages.shareText)"
:open-in-new-tab="false"
/>
<ModpackContentModal
ref="modpackContentModal"
:modpack-name="displayedModpackProject?.title"
:modpack-icon-url="displayedModpackProject?.icon_url ?? undefined"
<ManagedContentModal
ref="managedContentModal"
:header="managedContentModalHeader"
:source-name="managedContent?.card.manager.name"
:source-icon-url="managedContent?.card.manager.iconUrl"
:enable-toggle="!isServerInstance && !isSharedMember && !isQuarantined"
:busy="isBulkOperating"
:action-disabled="isBulkOperating || isInstanceBusy"
:get-overflow-options="getOverflowOptions"
:switch-version="
isServerInstance || isSharedMember || isQuarantined ? undefined : handleSwitchVersion
"
@update:enabled="handleModpackContentToggle"
@bulk:enable="(items) => handleModpackContentBulkToggle(items, true)"
@bulk:disable="(items) => handleModpackContentBulkToggle(items, false)"
@update:enabled="handleManagedContentToggle"
@bulk:enable="(items) => handleManagedContentBulkToggle(items, true)"
@bulk:disable="(items) => handleManagedContentBulkToggle(items, false)"
/>
<ConfirmDisableModal
ref="sharedDisableConfirmModal"
:count="pendingModpackDisableItems.length"
:count="pendingManagedContentDisableItems.length"
:item-type="formatMessage(messages.contentTypeProject)"
:warning="managedContentPolicy.disableWarning(pendingModpackDisableItems)"
:warning="managedContentPolicy.disableWarning(pendingManagedContentDisableItems)"
:action-disabled="isInstanceBusy"
@disable="confirmPendingModpackContentDisable"
@disable="confirmPendingManagedContentDisable"
/>
<ConfirmModpackUpdateModal
ref="modpackUpdateConfirmModal"
@@ -88,7 +89,7 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { ClipboardCopyIcon, FolderOpenIcon } from '@modrinth/assets'
import { ClipboardCopyIcon, FolderOpenIcon, LockIcon, LockOpenIcon } from '@modrinth/assets'
import {
type BulkOperationStatus,
commonMessages,
@@ -96,18 +97,20 @@ import {
ConfirmModpackUpdateModal,
ContentCardLayout as ContentPageLayout,
type ContentItem,
type ContentModpackCardCategory,
type ContentModpackCardProject,
type ContentModpackCardVersion,
type ContentOwner,
ContentUpdaterModal,
dedupeManagedContentItems,
defineMessages,
injectNotificationManager,
ModpackContentModal,
type ModpackContentModalState,
type ManagedContentData,
ManagedContentModal,
type ManagedContentModalState,
type ManagedContentProject,
type ManagedContentVersion,
type OverflowMenuOption,
provideContentManager,
ReadyTransition,
summarizeManagedContent,
UnknownFileWarningModal,
useDebugLogger,
useVIntl,
@@ -135,8 +138,10 @@ import {
add_project_from_path,
edit,
get_linked_modpack_content,
get_shared_instance_publish_preview,
is_file_on_modrinth,
remove_project,
set_project_locked,
switch_project_version_with_dependencies,
toggle_disable_project,
update_all,
@@ -152,8 +157,17 @@ import type { FeatureFlag } from '@/store/theme'
import { injectInstancePage } from '../instance-context'
import { instanceContentQueryOptions, instanceKeys } from '../query-options'
import { injectSharedInstance } from '../shared-instance-context'
const messages = defineMessages({
modpackContentHeader: {
id: 'app.instance.content.managed-content.modpack-header',
defaultMessage: 'Modpack content',
},
sharedContentHeader: {
id: 'app.instance.content.managed-content.shared-header',
defaultMessage: 'Shared content',
},
shareTitle: {
id: 'app.instance.mods.share-title',
defaultMessage: 'Sharing modpack content',
@@ -178,6 +192,14 @@ const messages = defineMessages({
id: 'app.instance.mods.locked-content',
defaultMessage: 'Content in locked instances cannot be changed.',
},
freezeContent: {
id: 'app.instance.mods.freeze-content',
defaultMessage: 'Freeze version',
},
unfreezeContent: {
id: 'app.instance.mods.unfreeze-content',
defaultMessage: 'Unfreeze version',
},
contentTypeProject: {
id: 'app.instance.mods.content-type-project',
defaultMessage: 'project',
@@ -196,7 +218,7 @@ const messages = defineMessages({
},
})
let savedModalState: ModpackContentModalState | null = null
let savedModalState: ManagedContentModalState | null = null
function contentOwnerLink(owner: ContentOwner): NonNullable<ContentOwner['link']> {
if (owner.type === 'user') return `/user/${encodeURIComponent(owner.id)}`
@@ -219,6 +241,7 @@ const skipNonEssentialWarnings = computed(() =>
)
const instancePage = injectInstancePage()
const sharedInstanceState = injectSharedInstance()
const instance = instancePage.instance
const isServerInstance = instancePage.isServerInstance
const openSettings = () => instancePage.openSettings(1)
@@ -283,15 +306,12 @@ watch(
},
)
const linkedModpackProject = ref<ContentModpackCardProject | null>(null)
const linkedModpackVersion = ref<ContentModpackCardVersion | null>(null)
const linkedModpackOwner = ref<ContentOwner | null>(null)
const linkedModpackCategories = ref<ContentModpackCardCategory[]>([])
const linkedModpackHasUpdate = ref(false)
const linkedModpackProject = ref<ManagedContentProject | null>(null)
const linkedModpackVersion = ref<ManagedContentVersion | null>(null)
const linkedModpackUpdateVersionId = ref<string | null>(null)
const localImportedModpackUnlinked = ref(false)
const localImportedModpackProject = computed<ContentModpackCardProject | null>(() => {
const localImportedModpackProject = computed<ManagedContentProject | null>(() => {
const link = instance.value.link
if (localImportedModpackUnlinked.value || link?.type !== 'imported_modpack') return null
@@ -300,7 +320,6 @@ const localImportedModpackProject = computed<ContentModpackCardProject | null>((
slug: link.filename ?? instance.value.id,
title: link.name ?? instance.value.name,
icon_url: instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : undefined,
description: '',
filename: link.filename ?? undefined,
}
})
@@ -319,6 +338,7 @@ watch(
const isModpackUpdating = ref(false)
const isBulkOperating = ref(false)
const isInstanceBusy = computed(() => instance.value?.install_stage !== 'installed')
const showSharedContentFilter = computed(() => instance.value.shared_instance?.role === 'member')
const isPackLocked = computed(
() =>
instance.value.quarantined ||
@@ -329,10 +349,10 @@ const isPackLocked = computed(
const shareModal = ref<InstanceType<typeof ShareModalWrapper> | null>()
const exportModal = ref(null)
const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal> | null>()
const modpackContentModal = ref<InstanceType<typeof ModpackContentModal> | null>()
const managedContentModal = ref<InstanceType<typeof ManagedContentModal> | null>()
const modpackUpdateConfirmModal = ref<InstanceType<typeof ConfirmModpackUpdateModal> | null>()
const sharedDisableConfirmModal = ref<InstanceType<typeof ConfirmDisableModal> | null>()
const pendingModpackDisableItems = ref<ContentItem[]>([])
const pendingManagedContentDisableItems = ref<ContentItem[]>([])
const unknownFileWarningModal = ref<InstanceType<typeof UnknownFileWarningModal> | null>()
const unknownFileName = ref('')
let resolveUnknownFileConfirmation: ((confirmed: boolean) => void) | null = null
@@ -349,6 +369,134 @@ const modpackContentQuery = useQuery({
),
})
const hasSharedManagedContent = computed(() => {
if (instance.value.shared_instance?.role === 'owner') return false
const linkType = instance.value.link?.type
return (
!!instance.value.shared_instance ||
linkType === 'server_project' ||
linkType === 'server_project_modpack'
)
})
const managedContentItems = computed(() => {
const linkedContent = modpackContentQuery.data.value ?? []
const sourcedContent = hasSharedManagedContent.value
? projects.value.filter((item) =>
['server_project', 'shared_instance'].includes(item.source_kind ?? ''),
)
: []
return dedupeManagedContentItems([...linkedContent, ...sourcedContent])
})
const managedContentSummary = computed(() =>
modpackContentQuery.isLoading.value && modpackContentQuery.data.value === undefined
? undefined
: summarizeManagedContent(managedContentItems.value),
)
const managedContent = computed<ManagedContentData | null>(() => {
const attachment = instance.value.shared_instance
const sharedManager = sharedInstanceState.manager.value
const linkedProject = instancePage.linkedProject.value
const linkType = instance.value.link?.type
const isSharedOwner = attachment?.role === 'owner'
if (
!isSharedOwner &&
(attachment || linkType === 'server_project' || linkType === 'server_project_modpack')
) {
const serverManaged =
sharedManager?.type === 'server' ||
!!attachment?.server_manager_name ||
linkType === 'server_project' ||
linkType === 'server_project_modpack' ||
(!attachment && isServerInstance.value)
const managerName = serverManaged
? (sharedManager?.name ??
attachment?.server_manager_name ??
linkedProject?.name ??
instance.value.name)
: (sharedManager?.name ?? instance.value.name)
const managerIcon = serverManaged
? (sharedManager?.avatarUrl ??
attachment?.server_manager_icon_url ??
linkedProject?.icon_url ??
undefined)
: (sharedManager?.avatarUrl ??
(instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : undefined))
const managerLink = serverManaged
? linkedProject
? {
path: `/project/${linkedProject.slug ?? linkedProject.id}`,
query: { i: instancePage.instanceId.value },
}
: undefined
: sharedManager?.type === 'user'
? `/user/${encodeURIComponent(sharedManager.name)}`
: undefined
return {
card: {
kind: serverManaged ? 'server' : 'shared-instance',
installing: isInstanceBusy.value,
manager: {
name: managerName,
iconUrl: managerIcon,
link: managerLink,
},
summary: managedContentSummary.value,
syncedAt: sharedInstanceState.lastUpdateCheckAt.value,
updateAvailable: instancePage.sharedInstanceUpdateAvailable.value,
},
disabled: attachment?.status === 'applying' || isInstanceBusy.value,
disabledText: formatMessage(commonMessages.updatingLabel),
}
}
const project = displayedModpackProject.value
if (!project) return null
return {
card: {
kind: 'modpack',
installing: isInstanceBusy.value,
manager: {
name: project.title,
iconUrl: project.icon_url ?? undefined,
link: linkedModpackProject.value
? {
path: `/project/${project.slug ?? project.id}`,
query: { i: instancePage.instanceId.value },
}
: undefined,
},
summary: managedContentSummary.value,
versionNumber: linkedModpackVersion.value?.version_number,
versionLink:
linkedModpackProject.value && linkedModpackVersion.value
? {
path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}/version/${linkedModpackVersion.value.id}`,
query: { i: instancePage.instanceId.value },
}
: undefined,
updatedAt: linkedModpackVersion.value?.date_published,
},
disabled: isModpackUpdating.value || isInstanceBusy.value,
disabledText: formatMessage(commonMessages.updatingLabel),
}
})
const managedContentModalHeader = computed(() =>
formatMessage(
managedContent.value?.card.kind === 'modpack'
? messages.modpackContentHeader
: messages.sharedContentHeader,
),
)
// TODO: Extract content operation and updater modal state into composables; this page currently owns file mutations, dependency installs, busy flags, and version selection flow.
const updatingProject = ref<ContentItem | null>(null)
const updatingProjectVersions = ref<Labrinth.Versions.v2.Version[]>([])
@@ -418,14 +566,30 @@ function canDeleteContent(item: ContentItem) {
return canMutateContent(item)
}
function canToggleContent(item: ContentItem) {
return canMutateContent(item)
}
function canChangeContentVersion(item: ContentItem) {
return canMutateContent(item) && !item.locked
}
async function reconcileSharedInstancePublishState() {
if (instance.value.shared_instance?.role !== 'owner') return
await get_shared_instance_publish_preview(instance.value.id).catch((error) => {
debug('Failed to reconcile shared instance publish state', { error })
})
}
function setContentItemBusy(item: ContentItem, busy: boolean, originalFileName = item.file_name) {
item.installing = busy
modpackContentModal.value?.updateItem(originalFileName, {
managedContentModal.value?.updateItem(originalFileName, {
installing: busy,
disabled: busy,
})
if (item.file_name !== originalFileName) {
modpackContentModal.value?.updateItem(item.file_name, {
managedContentModal.value?.updateItem(item.file_name, {
installing: busy,
disabled: busy,
})
@@ -627,8 +791,12 @@ async function handleUnknownFileContinue(dontShowAgain: boolean) {
resolveUnknownFileWarning(true)
}
async function toggleDisableMod(mod: ContentItem, desiredEnabled?: boolean) {
if (!mod.file_path) return
async function toggleDisableMod(
mod: ContentItem,
desiredEnabled?: boolean,
reconcileSharedState = true,
) {
if (!mod.file_path || !canToggleContent(mod)) return
const operation = beginContentOperation(mod)
if (!operation) return
const originalFilePath = mod.file_path
@@ -640,7 +808,7 @@ async function toggleDisableMod(mod: ContentItem, desiredEnabled?: boolean) {
mod.file_path = newPath
mod.file_name = newFileName
mod.enabled = enabled
modpackContentModal.value?.updateItem(operation.originalFileName, {
managedContentModal.value?.updateItem(operation.originalFileName, {
file_path: newPath,
file_name: newFileName,
enabled,
@@ -659,6 +827,10 @@ async function toggleDisableMod(mod: ContentItem, desiredEnabled?: boolean) {
project_type: mod.project_type,
disabled: !enabled,
})
if (reconcileSharedState) {
await reconcileSharedInstancePublishState()
}
} catch (err) {
handleError(err as Error)
} finally {
@@ -669,7 +841,7 @@ async function toggleDisableMod(mod: ContentItem, desiredEnabled?: boolean) {
const toggleDisableDebounced = toggleDisableMod
async function removeMod(mod: ContentItem) {
if (!mod.file_path) return
if (!mod.file_path || !canDeleteContent(mod)) return
const operation = beginContentOperation(mod)
if (!operation) return
@@ -800,7 +972,7 @@ async function bulkUpdateAllProjects(onProgress?: (status: BulkOperationStatus)
}
async function updateProject(mod: ContentItem) {
if (!canUpdateProject(mod)) return
if (!canUpdateProject(mod) || mod.locked) return
const operation = beginContentOperation(mod)
if (!operation) return
@@ -829,7 +1001,7 @@ async function updateProject(mod: ContentItem) {
}
async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions.v2.Version) {
if (!canMutateContent(mod)) return
if (!canChangeContentVersion(mod)) return
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
@@ -856,7 +1028,8 @@ async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions
async function handleUpdate(id: string) {
const item = projects.value.find((p) => getContentItemId(p) === id)
if (!item || !canUpdateProject(item) || !item.project?.id || !item.version?.id) return
if (!item || item.locked || !canUpdateProject(item) || !item.project?.id || !item.version?.id)
return
const requestId = beginUpdateRequest()
const itemId = getContentItemId(item)
@@ -966,7 +1139,7 @@ async function handleUpdate(id: string) {
}
async function handleSwitchVersion(item: ContentItem) {
if (!canMutateContent(item)) return
if (!canChangeContentVersion(item)) return
if (!item.project?.id || !item.version?.id) return
const requestId = beginUpdateRequest()
@@ -993,9 +1166,9 @@ async function handleSwitchVersion(item: ContentItem) {
updatingProjectVersions.value = versions
}
async function handleModpackContentToggle(item: ContentItem, enabled: boolean) {
async function handleManagedContentToggle(item: ContentItem, enabled: boolean) {
if (!enabled && managedContentPolicy.disableWarning([item])) {
pendingModpackDisableItems.value = [item]
pendingManagedContentDisableItems.value = [item]
sharedDisableConfirmModal.value?.show()
return
}
@@ -1003,47 +1176,48 @@ async function handleModpackContentToggle(item: ContentItem, enabled: boolean) {
await toggleDisableDebounced(item, enabled)
}
async function handleModpackContentBulkToggle(items: ContentItem[], enabled: boolean) {
async function handleManagedContentBulkToggle(items: ContentItem[], enabled: boolean) {
if (!enabled && managedContentPolicy.disableWarning(items)) {
pendingModpackDisableItems.value = items
pendingManagedContentDisableItems.value = items
sharedDisableConfirmModal.value?.show()
return
}
await setModpackContentEnabled(items, enabled)
await setManagedContentEnabled(items, enabled)
}
async function confirmPendingModpackContentDisable() {
const items = [...pendingModpackDisableItems.value]
pendingModpackDisableItems.value = []
await setModpackContentEnabled(items, false)
async function confirmPendingManagedContentDisable() {
const items = [...pendingManagedContentDisableItems.value]
pendingManagedContentDisableItems.value = []
await setManagedContentEnabled(items, false)
}
async function setModpackContentEnabled(items: ContentItem[], enabled: boolean) {
await Promise.all(items.map((item) => toggleDisableMod(item, enabled)))
async function setManagedContentEnabled(items: ContentItem[], enabled: boolean) {
await Promise.all(items.map((item) => toggleDisableMod(item, enabled, false)))
await reconcileSharedInstancePublishState()
}
async function handleModpackContent() {
async function handleManagedContent() {
if (!instance.value?.id) return
if (modpackContentQuery.data.value?.length) {
modpackContentModal.value?.show(modpackContentQuery.data.value)
if (modpackContentQuery.data.value !== undefined) {
managedContentModal.value?.show(managedContentItems.value)
return
}
modpackContentModal.value?.showLoading()
managedContentModal.value?.showLoading()
const { data, error } = await modpackContentQuery.refetch()
if (data !== undefined) {
modpackContentModal.value?.show(data)
managedContentModal.value?.show(managedContentItems.value)
} else {
if (error) handleError(error)
modpackContentModal.value?.hide()
managedContentModal.value?.hide()
}
}
async function refreshModpackContentItems(cacheBehaviour?: CacheBehaviour) {
async function refreshManagedContentItems(cacheBehaviour?: CacheBehaviour) {
if (!instance.value?.id) return
const contentItems = await queryClient
@@ -1054,13 +1228,13 @@ async function refreshModpackContentItems(cacheBehaviour?: CacheBehaviour) {
.catch(handleError)
if (contentItems) {
modpackContentModal.value?.setItems(contentItems)
managedContentModal.value?.setItems(managedContentItems.value)
}
}
async function refreshContentState(cacheBehaviour?: CacheBehaviour) {
await initProjects(cacheBehaviour)
await refreshModpackContentItems(cacheBehaviour)
await refreshManagedContentItems(cacheBehaviour)
}
watch(
@@ -1093,7 +1267,6 @@ async function handleModpackUpdate() {
linkedModpackUpdateVersionId: linkedModpackUpdateVersionId.value,
linkedModpackProject: linkedModpackProject.value,
linkedModpackVersion: linkedModpackVersion.value,
linkedModpackHasUpdate: linkedModpackHasUpdate.value,
instance: {
path: instance.value.id,
name: instance.value.name,
@@ -1262,8 +1435,6 @@ async function unpairInstance() {
})
linkedModpackProject.value = null
linkedModpackVersion.value = null
linkedModpackOwner.value = null
linkedModpackHasUpdate.value = false
linkedModpackUpdateVersionId.value = null
localImportedModpackUnlinked.value = true
await initProjects()
@@ -1326,9 +1497,41 @@ function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
})
}
if (canMutateContent(item)) {
options.push(
{ type: 'divider' },
{
id: item.locked ? 'unfreeze-content' : 'freeze-content',
label: formatMessage(item.locked ? messages.unfreezeContent : messages.freezeContent),
icon: item.locked ? LockOpenIcon : LockIcon,
action: () => handleContentFreeze(item, !item.locked),
},
)
}
return options
}
async function handleContentFreeze(item: ContentItem, frozen: boolean) {
if (!item.file_path || !canMutateContent(item)) return
const operation = beginContentOperation(item)
if (!operation) return
const originalFilePath = item.file_path
try {
await set_project_locked(instance.value.id, item.file_path, frozen)
item.locked = frozen
managedContentModal.value?.updateItem(operation.originalFileName, { locked: frozen })
updateLinkedModpackContentCache(item, operation.originalFileName, originalFilePath, {
locked: frozen,
})
} catch (err) {
handleError(err as Error)
} finally {
finishContentOperation(item, operation)
}
}
async function initProjects(cacheBehaviour?: CacheBehaviour, staleTime = 0) {
if (!instance.value) return
@@ -1358,16 +1561,10 @@ function applyContentData(contentData: InstanceContentData) {
if (contentData.modpack) {
linkedModpackProject.value = contentData.modpack.project
linkedModpackVersion.value = contentData.modpack.version
linkedModpackOwner.value = contentData.modpack.owner
linkedModpackCategories.value = contentData.modpack.categories
linkedModpackHasUpdate.value = contentData.modpack.hasUpdate
linkedModpackUpdateVersionId.value = contentData.modpack.updateVersionId
} else {
linkedModpackProject.value = null
linkedModpackVersion.value = null
linkedModpackOwner.value = null
linkedModpackCategories.value = []
linkedModpackHasUpdate.value = false
linkedModpackUpdateVersionId.value = null
}
@@ -1375,55 +1572,16 @@ function applyContentData(contentData: InstanceContentData) {
return true
}
function contentVersionLabel(item: ContentItem): string {
if (item.embedded_metadata?.version) return item.embedded_metadata.version
return formatMessage(commonMessages.unknownLabel)
}
provideContentManager({
items: mergedProjects,
loading,
error: ref(null),
modpack: computed(() => {
if (linkedModpackProject.value) {
return {
project: linkedModpackProject.value,
projectLink: {
path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}`,
query: { i: instancePage.instanceId.value },
},
version: linkedModpackVersion.value ?? undefined,
versionLink:
linkedModpackProject.value && linkedModpackVersion.value
? {
path: `/project/${linkedModpackProject.value.slug ?? linkedModpackProject.value.id}/version/${linkedModpackVersion.value.id}`,
query: { i: instancePage.instanceId.value },
}
: undefined,
owner: linkedModpackOwner.value
? {
...linkedModpackOwner.value,
link: contentOwnerLink(linkedModpackOwner.value),
}
: undefined,
categories: linkedModpackCategories.value,
hasUpdate: linkedModpackHasUpdate.value,
disabled: isModpackUpdating.value,
disabledText: isModpackUpdating.value
? formatMessage(commonMessages.updatingLabel)
: formatMessage(commonMessages.installingLabel),
}
}
if (localImportedModpackProject.value) {
return {
project: localImportedModpackProject.value,
categories: [],
hasUpdate: false,
disabled: isModpackUpdating.value,
disabledText: isModpackUpdating.value
? formatMessage(commonMessages.updatingLabel)
: formatMessage(commonMessages.installingLabel),
}
}
return null
}),
managedContent,
isPackLocked,
isBusy: isInstanceBusy,
disableAddContent: isQuarantined,
@@ -1432,23 +1590,27 @@ provideContentManager({
skipNonEssentialWarnings,
contentTypeLabel: ref(formatMessage(messages.contentTypeProject)),
toggleEnabled: toggleDisableDebounced,
bulkEnableItems: (items: ContentItem[]) =>
Promise.all(
bulkEnableItems: async (items: ContentItem[]) => {
await Promise.all(
items
.filter((item) => canMutateContent(item) && !item.enabled)
.map((item) => toggleDisableMod(item, true)),
).then(() => {}),
bulkDisableItems: (items: ContentItem[]) =>
Promise.all(
.filter((item) => canToggleContent(item) && !item.enabled)
.map((item) => toggleDisableMod(item, true, false)),
)
await reconcileSharedInstancePublishState()
},
bulkDisableItems: async (items: ContentItem[]) => {
await Promise.all(
items
.filter((item) => canMutateContent(item) && item.enabled)
.map((item) => toggleDisableMod(item, false)),
).then(() => {}),
.filter((item) => canToggleContent(item) && item.enabled)
.map((item) => toggleDisableMod(item, false, false)),
)
await reconcileSharedInstancePublishState()
},
deleteItem: removeMod,
bulkDeleteItems: (items: ContentItem[]) =>
Promise.all(items.filter(canMutateContent).map((item) => removeMod(item))).then(() => {}),
Promise.all(items.filter(canDeleteContent).map((item) => removeMod(item))).then(() => {}),
canDeleteItem: canDeleteContent,
canToggleItem: canMutateContent,
canToggleItem: canToggleContent,
getDeleteWarning: managedContentPolicy.deleteWarning,
getDisableWarning: managedContentPolicy.disableWarning,
getDeleteDependencyWarning,
@@ -1459,13 +1621,15 @@ provideContentManager({
updateItem: handleUpdate,
bulkUpdateAll: bulkUpdateAllProjects,
bulkUpdateItem: updateProject,
updateModpack:
isServerInstance.value || isSharedMember.value || isQuarantined.value
? undefined
: handleModpackUpdate,
viewModpackContent: handleModpackContent,
runManagedContentPrimaryAction:
instance.value.shared_instance?.role === 'member'
? instancePage.reviewSharedInstanceUpdate
: instance.value.link?.type === 'modrinth_modpack' && !isQuarantined.value
? handleModpackUpdate
: undefined,
viewManagedContent: handleManagedContent,
unlinkModpack: unpairInstance,
openSettings: openSettings,
openManagedContentSettings: openSettings,
switchVersion: handleSwitchVersion,
getOverflowOptions,
shareItems: handleShareItems,
@@ -1475,15 +1639,15 @@ provideContentManager({
project: item.project ?? {
id: item.file_name,
slug: null,
title: item.file_name.replace('.disabled', ''),
icon_url: null,
title: item.embedded_metadata?.name ?? item.file_name.replace('.disabled', ''),
icon_url: item.embedded_metadata?.icon_url ?? null,
},
projectLink: item.project?.id
? { path: `/project/${item.project.id}`, query: { i: instancePage.instanceId.value } }
: undefined,
version: item.version ?? {
id: item.file_name,
version_number: formatMessage(commonMessages.unknownLabel),
version_number: contentVersionLabel(item),
file_name: item.file_name,
},
versionLink:
@@ -1499,19 +1663,22 @@ provideContentManager({
link: contentOwnerLink(item.owner),
}
: undefined,
external: item.external ?? !item.project,
enabled: canMutateContent(item) ? item.enabled : undefined,
locked: item.locked,
installing: item.installing,
hideDelete: !canDeleteContent(item),
hideSwitchVersion: !canMutateContent(item) || !item.project?.id || !item.version?.id,
hasUpdate: canUpdateProject(item),
hideSwitchVersion: !canChangeContentVersion(item) || !item.project?.id || !item.version?.id,
hasUpdate: canUpdateProject(item) && !item.locked,
}),
showSharedContentFilter,
filterPersistKey: instance.value.id,
})
type UnlistenFn = () => void
const initialContentReady = loadInitialContent()
void initialContentReady.then(restoreModpackContentModalState).catch(handleError)
void initialContentReady.then(restoreManagedContentModalState).catch(handleError)
function getInstallRevision() {
return installRevisionByInstance.value.get(instance.value.id) ?? 0
@@ -1541,18 +1708,18 @@ watch(contentQuery.error, (error) => {
}
})
async function restoreModpackContentModalState() {
async function restoreManagedContentModalState() {
if (!savedModalState) return
const stateToRestore = savedModalState
savedModalState = null
await nextTick()
modpackContentModal.value?.restore(stateToRestore)
managedContentModal.value?.restore(stateToRestore)
}
// Save modal state when navigating away so it can be restored on back
const removeBeforeEach = router.beforeEach(() => {
const state = modpackContentModal.value?.getState()
const state = managedContentModal.value?.getState()
savedModalState = state ?? null
})
@@ -9,6 +9,7 @@ export interface InstancePageContext {
readonly instance: ComputedRef<GameInstance>
readonly linkedProject: ComputedRef<Labrinth.Projects.v3.Project | undefined>
readonly isServerInstance: ComputedRef<boolean>
readonly sharedInstanceUpdateAvailable: ComputedRef<boolean>
readonly offline: Readonly<Ref<boolean>>
readonly playing: ComputedRef<boolean>
readonly loading: Readonly<Ref<boolean>>
@@ -21,6 +22,7 @@ export interface InstancePageContext {
openSettings: (tab?: number) => void
browseContent: (projectType?: string) => Promise<void>
browseServers: () => Promise<void>
reviewSharedInstanceUpdate: (event?: MouseEvent) => void
}
export const [injectInstancePage, provideInstancePage] =
@@ -37,11 +37,8 @@
:loading-server-ping="loadingServerPing"
:players-online="playersOnline"
:status-online="statusOnline"
:recent-plays="recentPlays"
:ping="ping"
:minecraft-server="minecraftServer"
:linked-project-v3="linkedProjectV3"
:shared-instance-manager="sharedInstanceManager"
@repair="() => repairInstance()"
@stop="() => stopInstance('InstancePage')"
@play="() => startInstance('InstancePage')"
@@ -64,10 +61,8 @@
:shared-instance-expected-user-id="sharedInstanceExpectedUserId"
:shared-instance-role="instance.shared_instance?.role"
:shared-instance-signed-out="sharedInstanceSignedOut"
:shared-instance-update-available="showSharedInstanceUpdateAdmonition"
@published="refreshInstance"
@delete="requestInstanceDeletion"
@review-update="reviewSharedInstanceUpdate"
/>
</div>
<div :class="['p-6 pt-4', { 'min-h-0 flex-1 overflow-y-auto': isFixedRender }]">
@@ -211,7 +206,9 @@ useQuery(
retry: false,
})),
)
const linkedProjectId = computed(() => instance.value?.link?.project_id ?? '')
const linkedProjectId = computed(
() => instance.value?.link?.server_project_id ?? instance.value?.link?.project_id ?? '',
)
const linkedProjectQuery = useQuery(
computed(() => ({
...instanceLinkedProjectQueryOptions(linkedProjectId.value),
@@ -302,9 +299,6 @@ const minecraftServer = computed(() => linkedProjectV3.value?.minecraft_server)
const javaServerPingData = computed(() => linkedProjectV3.value?.minecraft_java_server?.ping?.data)
const liveServerStatusOnline = ref(false)
const statusOnline = computed(() => liveServerStatusOnline.value || !!javaServerPingData.value)
const recentPlays = computed(
() => linkedProjectV3.value?.minecraft_java_server?.verified_plays_2w ?? undefined,
)
const playersOnline = ref<number | undefined>(undefined)
const ping = ref<number | undefined>(undefined)
const loadingServerPing = ref(false)
@@ -317,7 +311,6 @@ provideSharedInstance(sharedInstanceState)
const {
actionsLocked: sharedInstanceActionsLocked,
expectedUserId: sharedInstanceExpectedUserId,
manager: sharedInstanceManager,
refreshUpdatePreview: refreshSharedInstanceUpdatePreview,
setUnavailable: setSharedInstanceUnavailable,
signedOut: sharedInstanceSignedOut,
@@ -331,7 +324,7 @@ const sharedInstanceUpdateKey = computed(() => {
const latestVersion = sharedInstanceUpdatePreview.value?.latestVersion
return instanceId && latestVersion !== undefined ? `${instanceId}:${latestVersion}` : null
})
const showSharedInstanceUpdateAdmonition = computed(
const sharedInstanceUpdateAvailable = computed(
() =>
sharedInstanceUpdatePreview.value?.updateAvailable === true &&
sharedInstanceUpdateKey.value !== hiddenSharedInstanceUpdateKey.value,
@@ -515,7 +508,7 @@ async function handleSharedInstanceUnavailable(
setSharedInstanceUnavailable(reason)
}
function reviewSharedInstanceUpdate(event: MouseEvent) {
function reviewSharedInstanceUpdate(event?: MouseEvent) {
const currentInstance = instance.value
const preview = sharedInstanceUpdatePreview.value
if (
@@ -792,6 +785,7 @@ provideInstancePage({
instance: instance as ComputedRef<GameInstance>,
linkedProject: linkedProjectV3,
isServerInstance,
sharedInstanceUpdateAvailable,
offline,
playing,
loading,
@@ -804,6 +798,7 @@ provideInstancePage({
openSettings,
browseContent,
browseServers,
reviewSharedInstanceUpdate,
})
provideInstanceBackup(() => instance.value!)
@@ -99,6 +99,7 @@ export function createSharedInstanceContext(
enabled: computed(
() =>
!!instance.value?.id &&
instance.value.install_stage === 'installed' &&
!!instance.value.shared_instance &&
!actionsLocked.value &&
!offline.value &&
@@ -128,6 +129,7 @@ export function createSharedInstanceContext(
const updatePreview = computed(() =>
unavailableReason.value ? null : (updatePreviewQuery.data.value ?? null),
)
const lastUpdateCheckAt = computed(() => updatePreviewQuery.dataUpdatedAt.value || undefined)
watch(
() => instance.value?.id,
@@ -162,6 +164,7 @@ export function createSharedInstanceContext(
unavailableManager,
manager,
updatePreview,
lastUpdateCheckAt,
expectedUserId,
wrongAccount,
signedOut,