mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
feat: sync individual content installation states on panel
This commit is contained in:
@@ -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
|
||||
|
||||
+243
-33
@@ -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' }) },
|
||||
|
||||
Reference in New Issue
Block a user