mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 01:54:47 +00:00
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:
@@ -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',
|
||||
|
||||
-32
@@ -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[]>(() => [
|
||||
{
|
||||
|
||||
+25
-42
@@ -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>
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user