feat: sync individual content installation states on panel

This commit is contained in:
Calum H. (IMB11)
2026-08-04 19:32:31 +01:00
parent ed936728ad
commit 526232535e
10 changed files with 350 additions and 52 deletions
@@ -1150,6 +1150,50 @@ export namespace Archon {
version_id: string
}
export type InstallProgressFileKey = {
type: 'file'
parent_directory: string
filename: string
install_type: 'install' | 'update'
}
export type InstallProgressModrinthModpackKey = {
type: 'modrinth_modpack'
project_id: string
version_id: string
}
export type InstallProgressLocalModpackKey = {
type: 'local_modpack'
filename: string
}
export type InstallProgressPlatformKey = {
type: 'platform'
platform: 'forge' | 'neoforge' | 'fabric' | 'quilt' | 'paper' | 'purpur' | 'vanilla'
platform_version: string
game_version: string
}
export type InstallProgressKey =
| InstallProgressFileKey
| InstallProgressModrinthModpackKey
| InstallProgressLocalModpackKey
| InstallProgressPlatformKey
export type InstallProgressItem = {
world_id: string
key: InstallProgressKey
id: string
progress: number | null
error: string | null
}
export type WSInstallProgressEvent = {
event: 'install-progress'
items: InstallProgressItem[]
}
export type FilesystemOpKind = 'unarchive'
export type FilesystemOpState =
@@ -1247,6 +1291,7 @@ export namespace Archon {
| WSInstallationResultEvent
| WSUptimeEvent
| WSNewModEvent
| WSInstallProgressEvent
| WSFilesystemOpsEvent
export type WSEventType = WSEvent['event']
@@ -96,6 +96,7 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
const fsAuth = ref<{ url: string; token: string } | null>(null)
const fsOps = ref<Archon.Websocket.v0.FilesystemOperation[]>([])
const fsQueuedOps = ref<Archon.Websocket.v0.QueuedFilesystemOp[]>([])
const installProgressItems = ref<Archon.Websocket.v0.InstallProgressItem[]>([])
const connectedSocketServerId = ref<string | null>(null)
const socketUnsubscribers = ref<SocketUnsubscriber[]>([])
const cpuData = ref<number[]>([])
@@ -265,6 +266,11 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
startUptimeTicker()
}
const handleInstallProgress = (data: Archon.Websocket.v0.WSInstallProgressEvent) => {
if (!shouldProcessEvent()) return
installProgressItems.value = data.items
}
const handleAuthIncorrect = () => {
if (!shouldProcessEvent()) return
isWsAuthIncorrect.value = true
@@ -301,6 +307,7 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
serverPowerState.value = 'stopped'
powerStateDetails.value = undefined
uptimeSeconds.value = 0
installProgressItems.value = []
}
const connectSocket = async (
@@ -317,6 +324,20 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
disconnectSocket(connectedSocketServerId.value ?? undefined)
try {
const baseSubscriptions: SocketUnsubscriber[] = [
client.archon.sockets.on(targetServerId, 'log', handleLog),
client.archon.sockets.on(targetServerId, 'log4j', handleLog4j),
client.archon.sockets.on(targetServerId, 'stats', handleStats),
client.archon.sockets.on(targetServerId, 'state', handleState),
client.archon.sockets.on(targetServerId, 'power-state', handlePowerState),
client.archon.sockets.on(targetServerId, 'uptime', handleUptime),
client.archon.sockets.on(targetServerId, 'install-progress', handleInstallProgress),
client.archon.sockets.on(targetServerId, 'auth-incorrect', handleAuthIncorrect),
client.archon.sockets.on(targetServerId, 'auth-ok', handleAuthOk),
]
const extraSubscriptions = connectOptions.extraSubscriptions?.(targetServerId) ?? []
socketUnsubscribers.value = [...baseSubscriptions, ...extraSubscriptions]
const safeConnectOptions = connectOptions.force ? { force: true } : undefined
await client.archon.sockets.safeConnect(targetServerId, safeConnectOptions)
connectedSocketServerId.value = targetServerId
@@ -326,21 +347,10 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
modrinthServersConsole.clear()
modrinthServersConsole.beginInitialLogHydration()
const baseSubscriptions: SocketUnsubscriber[] = [
client.archon.sockets.on(targetServerId, 'log', handleLog),
client.archon.sockets.on(targetServerId, 'log4j', handleLog4j),
client.archon.sockets.on(targetServerId, 'stats', handleStats),
client.archon.sockets.on(targetServerId, 'state', handleState),
client.archon.sockets.on(targetServerId, 'power-state', handlePowerState),
client.archon.sockets.on(targetServerId, 'uptime', handleUptime),
client.archon.sockets.on(targetServerId, 'auth-incorrect', handleAuthIncorrect),
client.archon.sockets.on(targetServerId, 'auth-ok', handleAuthOk),
]
const extraSubscriptions = connectOptions.extraSubscriptions?.(targetServerId) ?? []
socketUnsubscribers.value = [...baseSubscriptions, ...extraSubscriptions]
return true
} catch (error) {
console.error('[hosting/manage] Failed to connect server socket:', error)
clearSocketListeners()
isConnected.value = false
return false
}
@@ -402,6 +412,7 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
isServerRunning,
stats,
uptimeSeconds,
installProgressItems,
isSyncingContent: options.isSyncingContent as Ref<boolean>,
busyReasons,
fsAuth,
@@ -437,6 +448,7 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
isConnected,
isServerRunning,
isWsAuthIncorrect,
installProgressItems,
powerStateDetails,
ramData,
refreshFsAuth,
@@ -17,6 +17,7 @@ import Avatar from '#ui/components/base/Avatar.vue'
import BulletDivider from '#ui/components/base/BulletDivider.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
import Checkbox from '#ui/components/base/Checkbox.vue'
import ProgressSpinner from '#ui/components/base/ProgressSpinner.vue'
import type { Option as OverflowMenuOption } from '#ui/components/base/OverflowMenu.vue'
import TeleportOverflowMenu from '#ui/components/base/TeleportOverflowMenu.vue'
import Toggle from '#ui/components/base/Toggle.vue'
@@ -50,6 +51,7 @@ interface Props {
source?: ContentSource
enabled?: boolean
installing?: boolean
installProgress?: number | null
hasUpdate?: boolean
isClientOnly?: boolean
clientWarning?: ClientWarningType | null
@@ -73,6 +75,7 @@ const props = withDefaults(defineProps<Props>(), {
source: undefined,
enabled: undefined,
installing: false,
installProgress: undefined,
hasUpdate: false,
isClientOnly: false,
clientWarning: null,
@@ -124,6 +127,11 @@ const clientWarningMessage = computed(() => {
const { shift: shiftHeld } = useMagicKeys()
const deleteHovered = ref(false)
const installTooltip = computed(() => {
if (!props.installing) return undefined
if (props.installProgress == null) return formatMessage(commonMessages.installingLabel)
return `${formatMessage(commonMessages.installingLabel)} (${Math.round(props.installProgress)}%)`
})
</script>
<template>
@@ -147,6 +155,7 @@ const deleteHovered = ref(false)
v-if="showCheckbox"
:model-value="selected ?? false"
:aria-label="formatMessage(messages.selectProject, { project: project.title })"
:disabled="isDisabled"
class="shrink-0"
@update:model-value="(value, event) => emit('select', value, event)"
/>
@@ -156,7 +165,7 @@ const deleteHovered = ref(false)
:class="enabled === false && !disabled ? 'grayscale opacity-50' : ''"
>
<div
v-tooltip="installing ? formatMessage(commonMessages.installingLabel) : undefined"
v-tooltip="installTooltip"
class="relative flex shrink-0 items-center"
>
<Avatar
@@ -170,7 +179,13 @@ const deleteHovered = ref(false)
v-if="installing"
class="absolute inset-0 flex items-center justify-center rounded-2xl bg-black/20"
>
<SpinnerIcon class="size-5 animate-spin text-white" />
<ProgressSpinner
v-if="installProgress != null && installProgress > 0"
:progress="installProgress"
:max="100"
class="size-5 text-white"
/>
<SpinnerIcon v-else class="size-5 animate-spin text-white" />
</div>
</div>
<div class="flex min-w-0 flex-col gap-0.5">
@@ -104,20 +104,24 @@ defineExpose({
})
// Selection logic
const selectableItems = computed(() => props.items.filter((item) => !item.disabled))
const allSelected = computed(() => {
if (props.items.length === 0) return false
return props.items.every((item) => selectedIds.value.includes(item.id))
if (selectableItems.value.length === 0) return false
return selectableItems.value.every((item) => selectedIds.value.includes(item.id))
})
const someSelected = computed(() => {
return props.items.some((item) => selectedIds.value.includes(item.id)) && !allSelected.value
return (
selectableItems.value.some((item) => selectedIds.value.includes(item.id)) && !allSelected.value
)
})
function toggleSelectAll() {
if (allSelected.value || someSelected.value) {
selectedIds.value = []
} else {
selectedIds.value = props.items.map((item) => item.id)
selectedIds.value = selectableItems.value.map((item) => item.id)
}
}
@@ -132,7 +136,10 @@ function toggleItemSelection(
if (selected && event?.shiftKey && lastSelectedIndex.value !== null && index !== undefined) {
const start = Math.min(lastSelectedIndex.value, index)
const end = Math.max(lastSelectedIndex.value, index)
const rangeIds = props.items.slice(start, end + 1).map((item) => item.id)
const rangeIds = props.items
.slice(start, end + 1)
.filter((item) => !item.disabled)
.map((item) => item.id)
const merged = new Set([...selectedIds.value, ...rangeIds])
selectedIds.value = [...merged]
} else if (selected) {
@@ -192,6 +199,7 @@ function handleSort(column: ContentCardTableSortColumn) {
:model-value="allSelected"
:indeterminate="someSelected"
:aria-label="formatMessage(commonMessages.selectAllLabel)"
:disabled="selectableItems.length === 0"
class="shrink-0"
@update:model-value="toggleSelectAll"
/>
@@ -267,6 +275,7 @@ function handleSort(column: ContentCardTableSortColumn) {
:source="item.source"
:enabled="item.enabled"
:installing="item.installing"
:install-progress="item.installProgress"
:has-update="item.hasUpdate"
:is-client-only="item.isClientOnly"
:client-warning="item.clientWarning"
@@ -331,6 +340,7 @@ function handleSort(column: ContentCardTableSortColumn) {
:source="item.source"
:enabled="item.enabled"
:installing="item.installing"
:install-progress="item.installProgress"
:has-update="item.hasUpdate"
:is-client-only="item.isClientOnly"
:client-warning="item.clientWarning"
@@ -286,6 +286,7 @@ const tableItems = computed<ContentCardTableItem[]>(() => {
toggleDisabled: ctx.isBusy.value,
toggleDisabledTooltip: ctx.isBusy.value ? (ctx.busyMessage?.value ?? null) : null,
installing: item.installing === true,
installProgress: item.installProgress,
hasUpdate: base.hasUpdate ?? item.has_update,
isClientOnly:
isClientOnlyEnvironment(item.environment) ||
@@ -56,6 +56,7 @@ export interface ContentCardTableItem {
toggleDisabled?: boolean
toggleDisabledTooltip?: string | null
installing?: boolean
installProgress?: number | null
hasUpdate?: boolean
isClientOnly?: boolean
clientWarning?: ClientWarningType | null
@@ -91,6 +92,7 @@ export interface ContentItem extends Omit<
pack_client_retained?: boolean
pack_client_depends?: boolean
installing?: boolean
installProgress?: number | null
source_kind?: ContentSourceKind | null
external?: boolean
external_url?: string
@@ -115,8 +115,16 @@ const messages = defineMessages({
})
const client = injectModrinthClient()
const { server, serverId, worldId, busyReasons, isSyncingContent, uploadState, cancelUpload } =
injectModrinthServerContext()
const {
server,
serverId,
worldId,
busyReasons,
isSyncingContent,
installProgressItems,
uploadState,
cancelUpload,
} = injectModrinthServerContext()
const contentUploadSession = useUploadSessionUpload({
client,
scope: 'content',
@@ -202,6 +210,45 @@ const setupActionBusyMessage = computed(() => {
return filteredReasons.length > 0 ? formatMessage(filteredReasons[0].reason) : null
})
const currentWorldInstallProgressItems = computed(() =>
installProgressItems.value.filter((item) => item.world_id === worldId.value),
)
const isIndividualContentSync = ref(false)
watch(
[currentWorldInstallProgressItems, isSyncingContent],
([items, syncing]) => {
if (!syncing) {
isIndividualContentSync.value = false
return
}
if (items.some((item) => item.key.type !== 'file')) {
isIndividualContentSync.value = false
return
}
if (items.length > 0) {
isIndividualContentSync.value = true
}
},
{ immediate: true },
)
const contentBusyReasons = computed(() => {
if (!isIndividualContentSync.value) return busyReasons.value
return busyReasons.value.filter(
(reason) =>
reason.reason.id !== 'servers.busy.installing' &&
reason.reason.id !== 'servers.busy.syncing-content',
)
})
const contentActionDisabled = computed(() => !canSetup.value || contentBusyReasons.value.length > 0)
const contentActionBusyMessage = computed(() => {
if (!canSetup.value) return permissionDeniedMessage.value
return contentBusyReasons.value.length > 0
? formatMessage(contentBusyReasons.value[0].reason)
: null
})
const modpackProjectId = computed(() => {
const spec = contentQuery.data.value?.modpack?.spec
return spec?.platform === 'modrinth' ? spec.project_id : null
@@ -308,6 +355,9 @@ const modpack = computed<ContentModpackData | null>(() => {
header: 'categories',
})) as ContentModpackCardCategory[],
hasUpdate: !!mp.has_update || !!newestModpackUpdateVersion.value,
disabled: setupActionDisabled.value,
disabledText:
setupActionBusyMessage.value ?? formatMessage(commonMessages.installingLabel),
}
})
@@ -332,6 +382,94 @@ const addonLookup = computed(() => {
return map
})
function normalizeInstallFilename(filename: string) {
return filename.endsWith('.disabled') ? filename.slice(0, -'.disabled'.length) : filename
}
const fileInstallProgressByFilename = computed(() => {
const progressByFilename = new Map<string, Archon.Websocket.v0.InstallProgressItem>()
for (const item of currentWorldInstallProgressItems.value) {
if (item.key.type === 'file') {
progressByFilename.set(normalizeInstallFilename(item.key.filename), item)
}
}
return progressByFilename
})
const completedInstallFilenames = ref<Set<string>>(new Set())
watch(
fileInstallProgressByFilename,
(progressByFilename, previousProgressByFilename) => {
const completed = new Set(completedInstallFilenames.value)
let shouldRefreshContent = false
if (previousProgressByFilename) {
for (const filename of previousProgressByFilename.keys()) {
if (!progressByFilename.has(filename)) {
if (!completed.has(filename)) shouldRefreshContent = true
completed.add(filename)
}
}
}
for (const [filename, item] of progressByFilename) {
if (item.error != null || item.progress === 100) {
if (item.error == null && !completed.has(filename)) shouldRefreshContent = true
completed.add(filename)
} else {
completed.delete(filename)
}
}
completedInstallFilenames.value = completed
if (shouldRefreshContent && !contentQuery.isFetching.value) {
void contentQuery.refetch()
}
},
{ immediate: true },
)
function getContentItemInstallFilename(item: ContentItem) {
const filename = item.version?.file_name || item.file_name
return normalizeInstallFilename(filename)
}
function getContentItemInstallProgress(item: ContentItem) {
return fileInstallProgressByFilename.value.get(getContentItemInstallFilename(item))
}
function fileInstallProgressToContentItem(
item: Archon.Websocket.v0.InstallProgressItem,
key: Archon.Websocket.v0.InstallProgressFileKey,
): ContentItem {
const filename = key.filename
const extensionIndex = filename.lastIndexOf('.')
const title = extensionIndex > 0 ? filename.slice(0, extensionIndex) : filename
const projectType =
key.parent_directory === 'plugins'
? 'plugin'
: key.parent_directory === 'datapacks'
? 'datapack'
: 'mod'
return {
id: `installing:${item.id}`,
file_name: filename,
project: {
id: item.id,
slug: filename,
title,
},
version: {
id: item.id,
version_number: formatMessage(commonMessages.installingLabel),
file_name: filename,
},
enabled: true,
project_type: projectType,
has_update: false,
update_version_id: null,
installing: true,
installProgress: item.progress,
}
}
const pendingServerContentInstalls = ref<PendingServerContentInstall[]>([])
const lastStableContentKeys = ref<Set<string>>(new Set())
const contentInstallBaselineKeys = ref<Set<string> | null>(null)
@@ -347,7 +485,19 @@ const { pause: pausePendingInstallPoll, resume: resumePendingInstallPoll } = use
)
function syncPendingServerContentInstalls() {
pendingServerContentInstalls.value = readPendingServerContentInstalls(serverId, worldId.value)
const pendingInstalls = readPendingServerContentInstalls(serverId, worldId.value)
pendingServerContentInstalls.value = pendingInstalls
const completed = new Set(completedInstallFilenames.value)
let changed = false
for (const item of pendingInstalls) {
if (item.fileName) {
changed = completed.delete(normalizeInstallFilename(item.fileName)) || changed
}
}
if (changed) {
completedInstallFilenames.value = completed
}
}
function handlePendingServerContentInstallsChanged(event: Event) {
@@ -580,12 +730,19 @@ const rawContentItems = computed<ContentItem[]>(() => {
(addon.version?.id ? pendingInstallByVersionId.get(addon.version.id) : null) ??
pendingInstallByFileName.get(addon.filename) ??
null
const installing = !!pendingItem || installingContentKeys.has(getAddonInstallKey(addon))
const installProgress = getContentItemInstallProgress(contentItem)
const installFilename = getContentItemInstallFilename(contentItem)
const installing =
installProgress != null
? installProgress.error == null && installProgress.progress !== 100
: !completedInstallFilenames.value.has(installFilename) &&
(!!pendingItem || installingContentKeys.has(getAddonInstallKey(addon)))
if (!installing || !pendingItem) {
return {
...contentItem,
installing,
installProgress: installing ? installProgress?.progress : undefined,
}
}
@@ -611,10 +768,38 @@ const rawContentItems = computed<ContentItem[]>(() => {
},
owner: pendingContentItem.owner ?? contentItem.owner,
installing,
installProgress: installProgress?.progress,
}
})
return [...addonItems, ...pendingItems]
const pendingDisplayItems = pendingItems.map((item) => {
const installProgress = getContentItemInstallProgress(item)
const installing =
installProgress != null
? installProgress.error == null && installProgress.progress !== 100
: !completedInstallFilenames.value.has(getContentItemInstallFilename(item))
return {
...item,
installing,
installProgress: installing ? installProgress?.progress : undefined,
}
})
const displayedInstallFilenames = new Set(
[...addonItems, ...pendingDisplayItems].map(getContentItemInstallFilename),
)
const progressOnlyItems = currentWorldInstallProgressItems.value.flatMap((item) => {
if (
item.key.type !== 'file' ||
item.error != null ||
item.progress === 100 ||
displayedInstallFilenames.has(normalizeInstallFilename(item.key.filename))
) {
return []
}
return [fileInstallProgressToContentItem(item, item.key)]
})
return [...addonItems, ...pendingDisplayItems, ...progressOnlyItems]
})
const displayedContentItems = ref<ContentItem[]>([])
@@ -637,13 +822,31 @@ function getContentItemId(item: ContentItem) {
function mergeFragileContentItems(items: ContentItem[]) {
const nextItems = new Map(items.map((item) => [getContentItemDisplayKey(item), item]))
const mergedItems = displayedContentItems.value.map((item) => {
const key = getContentItemDisplayKey(item)
const nextItem = nextItems.get(key)
if (!nextItem) return item
const mergedItems = displayedContentItems.value.flatMap((item) => {
let nextKey = getContentItemDisplayKey(item)
let nextItem = nextItems.get(nextKey)
if (!nextItem && item.installing) {
const installFilename = getContentItemInstallFilename(item)
const matchingEntry = Array.from(nextItems.entries()).find(
([, candidate]) => getContentItemInstallFilename(candidate) === installFilename,
)
if (matchingEntry) {
nextKey = matchingEntry[0]
nextItem = matchingEntry[1]
}
}
if (!nextItem) {
if (
item.installing &&
completedInstallFilenames.value.has(getContentItemInstallFilename(item))
) {
return []
}
return [item]
}
nextItems.delete(key)
return nextItem
nextItems.delete(nextKey)
return [nextItem]
})
return [...mergedItems, ...nextItems.values()]
@@ -658,9 +861,7 @@ watch(
],
([items, syncing, isFetching, isLoading]) => {
if (syncing) {
if (items.length > 0) {
displayedContentItems.value = mergeFragileContentItems(items)
}
displayedContentItems.value = mergeFragileContentItems(items)
return
}
@@ -702,6 +903,7 @@ watch(
watch(
worldId,
() => {
completedInstallFilenames.value = new Set()
syncPendingServerContentInstalls()
syncContentInstallKeys()
void flushStoredServerInstalls()
@@ -793,14 +995,14 @@ const toggleMutation = useMutation({
})
async function handleToggleEnabled(item: ContentItem) {
if (setupActionDisabled.value) return
if (contentActionDisabled.value) return
const addon = addonLookup.value.get(item.file_name)
if (!addon) return
await toggleMutation.mutateAsync({ addon })
}
async function handleDeleteItem(item: ContentItem) {
if (setupActionDisabled.value) return
if (contentActionDisabled.value) return
const addon = addonLookup.value.get(item.file_name)
if (!addon) return
await deleteMutation.mutateAsync({ addon })
@@ -808,6 +1010,7 @@ async function handleDeleteItem(item: ContentItem) {
function itemsToAddonRequests(items: ContentItem[]): Archon.Content.v1.RemoveAddonRequest[] {
return items.flatMap((item) => {
if (item.installing) return []
const addon = addonLookup.value.get(item.file_name)
if (!addon) return []
return [{ filename: addon.filename, kind: addon.kind }]
@@ -815,7 +1018,7 @@ function itemsToAddonRequests(items: ContentItem[]): Archon.Content.v1.RemoveAdd
}
async function handleBulkDelete(items: ContentItem[]) {
if (setupActionDisabled.value) return
if (contentActionDisabled.value) return
const requests = itemsToAddonRequests(items)
if (requests.length === 0) return
try {
@@ -831,7 +1034,7 @@ async function handleBulkDelete(items: ContentItem[]) {
}
async function handleBulkEnable(items: ContentItem[]) {
if (setupActionDisabled.value) return
if (contentActionDisabled.value) return
const requests = itemsToAddonRequests(items)
if (requests.length === 0) return
try {
@@ -847,7 +1050,7 @@ async function handleBulkEnable(items: ContentItem[]) {
}
async function handleBulkDisable(items: ContentItem[]) {
if (setupActionDisabled.value) return
if (contentActionDisabled.value) return
const requests = itemsToAddonRequests(items)
if (requests.length === 0) return
try {
@@ -916,7 +1119,7 @@ const currentLoader = computed(
)
function handleBrowseContent() {
if (setupActionDisabled.value) return
if (contentActionDisabled.value) return
const contentType = type.value
if (browseServerContent && ['mod', 'plugin', 'datapack'].includes(contentType)) {
browseServerContent({
@@ -934,7 +1137,7 @@ function handleBrowseContent() {
}
function handleUploadFiles() {
if (setupActionDisabled.value) return
if (contentActionDisabled.value) return
const input = document.createElement('input')
input.type = 'file'
input.multiple = true
@@ -1074,7 +1277,7 @@ async function handleViewModpackContent() {
}
async function handleModpackContentToggle(item: ContentItem) {
if (setupActionDisabled.value) return
if (contentActionDisabled.value) return
const addon = addonLookup.value.get(item.file_name)
if (!addon) return
modpackContentModal.value?.updateItem(item.file_name, { disabled: true })
@@ -1105,7 +1308,7 @@ async function handleModpackContentToggle(item: ContentItem) {
}
async function handleModpackBulkToggle(items: ContentItem[], enable: boolean) {
if (setupActionDisabled.value) return
if (contentActionDisabled.value) return
const requests = itemsToAddonRequests(items)
if (requests.length === 0) return
@@ -1172,9 +1375,9 @@ async function handleModpackUnlinkConfirm() {
}
async function handleBulkUpdate(items: ContentItem[]) {
if (setupActionDisabled.value) return
if (contentActionDisabled.value) return
const addons = items
.filter((item) => item.has_update)
.filter((item) => item.has_update && !item.installing)
.map((item) => ({
filename: item.file_name,
version_id: item.update_version_id ?? undefined,
@@ -1218,6 +1421,7 @@ async function handleSwitchVersion(item: ContentItem) {
}
async function handleModpackUpdate() {
if (setupActionDisabled.value) return
const mp = contentQuery.data.value?.modpack
if (!mp || mp.spec.platform !== 'modrinth') return
@@ -1272,8 +1476,8 @@ function resetUpdateState() {
}
function handleModalUpdate(selectedVersion: Labrinth.Versions.v2.Version, event?: MouseEvent) {
if (setupActionDisabled.value) return
if (updatingModpack.value) {
if (setupActionDisabled.value) return
pendingModpackUpdateVersion.value = selectedVersion
const mpSpec = contentQuery.data.value?.modpack?.spec
@@ -1294,6 +1498,7 @@ function handleModalUpdate(selectedVersion: Labrinth.Versions.v2.Version, event?
return
}
if (contentActionDisabled.value) return
performUpdate(selectedVersion)
}
@@ -1310,7 +1515,10 @@ function setAddonInstalling(filename: string, installing: boolean) {
}
async function performUpdate(selectedVersion: Labrinth.Versions.v2.Version) {
if (setupActionDisabled.value) return
if (
(updatingModpack.value && setupActionDisabled.value) ||
(!updatingModpack.value && contentActionDisabled.value)
) return
const item = updatingProject.value
if (item) {
setAddonInstalling(item.file_name, true)
@@ -1389,8 +1597,8 @@ provideContentManager({
error: computed(() => contentQuery.error.value ?? null),
modpack,
isPackLocked: ref(false),
isBusy: setupActionDisabled,
busyMessage: setupActionBusyMessage,
isBusy: contentActionDisabled,
busyMessage: contentActionBusyMessage,
disableAddContent: computed(() => !canSetup.value),
disableAddContentTooltip: permissionDeniedMessage.value,
contentTypeLabel: type,
@@ -1462,8 +1670,8 @@ provideContentManager({
:modpack-name="modpack?.project.title"
:modpack-icon-url="modpack?.project.icon_url"
enable-toggle
:action-disabled="setupActionDisabled"
:action-disabled-tooltip="setupActionBusyMessage ?? undefined"
:action-disabled="contentActionDisabled"
:action-disabled-tooltip="contentActionBusyMessage ?? undefined"
@update:enabled="handleModpackContentToggle"
@bulk:enable="handleModpackBulkToggle($event, true)"
@bulk:disable="handleModpackBulkToggle($event, false)"
@@ -1494,8 +1702,10 @@ provideContentManager({
"
:loading="loadingVersions"
:loading-changelog="loadingChangelog"
:action-disabled="setupActionDisabled"
:action-disabled-tooltip="setupActionBusyMessage ?? undefined"
:action-disabled="updatingModpack ? setupActionDisabled : contentActionDisabled"
:action-disabled-tooltip="
(updatingModpack ? setupActionBusyMessage : contentActionBusyMessage) ?? undefined
"
target-type="instance"
@update="handleModalUpdate"
@cancel="resetUpdateState"
@@ -49,6 +49,7 @@ export interface ModrinthServerContext {
readonly isServerRunning: ComputedRef<boolean>
readonly stats: Ref<ServerStats>
readonly uptimeSeconds: Ref<number>
readonly installProgressItems: Ref<Archon.Websocket.v0.InstallProgressItem[]>
// Content sync state
readonly isSyncingContent: Ref<boolean>
@@ -71,6 +71,7 @@ const meta = {
isServerRunning: computed(() => true),
stats,
uptimeSeconds: ref(0),
installProgressItems: ref<Archon.Websocket.v0.InstallProgressItem[]>([]),
isSyncingContent: ref(false),
busyReasons: computed(() => []),
fsAuth: ref(null),
@@ -104,6 +104,7 @@ const meta = {
isServerRunning: computed(() => true),
stats,
uptimeSeconds: ref(0),
installProgressItems: ref<Archon.Websocket.v0.InstallProgressItem[]>([]),
isSyncingContent: ref(false),
busyReasons: computed(() => [
{ reason: defineMessage({ id: 's.bg', defaultMessage: 'Background task running' }) },