mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 02:24:56 +00:00
feat: content management changes (#6104)
* feat: change modpack updating flow * fix: pending install state loss * fix: mods.vue perf problems * chore: todo doc * draft: try preload/fix suspense * fix: lint
This commit is contained in:
+126
-24
@@ -32,7 +32,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-4 pb-16">
|
||||
<div class="flex-1 overflow-y-auto px-4" :class="isModpack ? 'pb-4' : 'pb-16'">
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center h-full gap-2">
|
||||
<SpinnerIcon class="h-8 w-8 animate-spin text-secondary" />
|
||||
<span class="text-sm text-secondary">{{
|
||||
@@ -76,11 +76,11 @@
|
||||
class="rounded-full text-sm font-medium flex items-center flex-shrink-0 border border-solid"
|
||||
:class="[
|
||||
getBadgeClasses(version),
|
||||
isVersionCompatible(version) ? 'px-2.5 py-0.5' : 'p-1',
|
||||
shouldShowIncompatibleBadge(version) ? 'p-1' : 'px-2.5 py-0.5',
|
||||
]"
|
||||
>
|
||||
<CircleAlertIcon
|
||||
v-if="!isVersionCompatible(version)"
|
||||
v-if="shouldShowIncompatibleBadge(version)"
|
||||
v-tooltip="formatMessage(messages.incompatibleBadge)"
|
||||
class="size-4"
|
||||
/>
|
||||
@@ -99,6 +99,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isModpack"
|
||||
class="absolute bottom-0 left-0 right-0 pointer-events-none flex flex-col items-center justify-end bg-gradient-to-b from-transparent to-bg-raised to-70% pb-3 h-24"
|
||||
>
|
||||
<div class="pointer-events-auto">
|
||||
@@ -197,13 +198,16 @@
|
||||
<div
|
||||
class="w-full flex flex-row items-center gap-4 p-4 border-solid border-x-0 border-b-0 border-t border-surface-4"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2 max-w-[55%] flex-1 text-orange mr-auto">
|
||||
<div
|
||||
v-if="showUpdateWarning"
|
||||
class="flex flex-row items-center gap-2 max-w-[55%] flex-1 text-orange mr-auto"
|
||||
>
|
||||
<TriangleAlertIcon class="size-6 shrink-0" />
|
||||
<span>{{
|
||||
formatMessage(isApp ? messages.updateWarningApp : messages.updateWarningWeb)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 shrink-0">
|
||||
<div class="flex flex-row gap-2 shrink-0 ml-auto">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="handleCancel">
|
||||
<XIcon />
|
||||
@@ -233,6 +237,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
|
||||
<ConfirmModal
|
||||
ref="incompatibleUpdateModal"
|
||||
:title="formatMessage(messages.incompatibleUpdateHeader)"
|
||||
:description="
|
||||
formatMessage(messages.incompatibleUpdateDescription, {
|
||||
version: pendingIncompatibleUpdate?.version.version_number ?? '...',
|
||||
})
|
||||
"
|
||||
:proceed-icon="DownloadIcon"
|
||||
:proceed-label="formatMessage(messages.updateAnywayButton)"
|
||||
:danger="false"
|
||||
:markdown="false"
|
||||
@proceed="confirmIncompatibleUpdate"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -255,11 +274,16 @@ import { computed, ref, watch } from 'vue'
|
||||
import Avatar from '#ui/components/base/Avatar.vue'
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import StyledInput from '#ui/components/base/StyledInput.vue'
|
||||
import ConfirmModal from '#ui/components/modal/ConfirmModal.vue'
|
||||
import NewModal from '#ui/components/modal/NewModal.vue'
|
||||
import VersionChannelIndicator from '#ui/components/version/VersionChannelIndicator.vue'
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
import {
|
||||
versionChangesGameVersion,
|
||||
versionMatchesCompatibilityTarget,
|
||||
} from '#ui/utils/version-compatibility'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const debug = useDebugLogger('ContentUpdaterModal')
|
||||
@@ -338,6 +362,19 @@ const messages = defineMessages({
|
||||
id: 'instances.updater-modal.loading-changelog',
|
||||
defaultMessage: 'Loading changelog...',
|
||||
},
|
||||
incompatibleUpdateHeader: {
|
||||
id: 'instances.updater-modal.incompatible-update.header',
|
||||
defaultMessage: 'Update to incompatible version?',
|
||||
},
|
||||
incompatibleUpdateDescription: {
|
||||
id: 'instances.updater-modal.incompatible-update.description',
|
||||
defaultMessage:
|
||||
'{version} is not marked as compatible with this installation. It may fail to launch or behave unexpectedly.',
|
||||
},
|
||||
updateAnywayButton: {
|
||||
id: 'instances.updater-modal.incompatible-update.proceed',
|
||||
defaultMessage: 'Update anyway',
|
||||
},
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -378,10 +415,15 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const incompatibleUpdateModal = ref<InstanceType<typeof ConfirmModal>>()
|
||||
const searchQuery = ref('')
|
||||
const hideIncompatibleState = ref(true)
|
||||
const switchMode = ref(false)
|
||||
const selectedVersion = ref<Labrinth.Versions.v2.Version | null>(null)
|
||||
const pendingIncompatibleUpdate = ref<{
|
||||
version: Labrinth.Versions.v2.Version
|
||||
event: MouseEvent
|
||||
} | null>(null)
|
||||
// Store the initial version ID to select when versions become available
|
||||
const pendingInitialVersionId = ref<string | undefined>(undefined)
|
||||
|
||||
@@ -422,15 +464,13 @@ watch(
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const NON_MOD_PROJECT_TYPES = new Set(['shader', 'shaderpack', 'resourcepack', 'datapack'])
|
||||
|
||||
function isVersionCompatible(version: Labrinth.Versions.v2.Version): boolean {
|
||||
const hasGameVersion = version.game_versions.includes(props.currentGameVersion)
|
||||
const skipLoaderCheck = props.projectType != null && NON_MOD_PROJECT_TYPES.has(props.projectType)
|
||||
const hasLoader =
|
||||
skipLoaderCheck ||
|
||||
version.loaders.some((loader) => loader.toLowerCase() === props.currentLoader.toLowerCase())
|
||||
const compatible = hasGameVersion && hasLoader
|
||||
const compatible = versionMatchesCompatibilityTarget(version, {
|
||||
gameVersion: props.currentGameVersion,
|
||||
loader: props.currentLoader,
|
||||
projectType: props.projectType,
|
||||
})
|
||||
|
||||
if (!compatible) {
|
||||
debug('isVersionCompatible: INCOMPATIBLE', {
|
||||
versionId: version.id,
|
||||
@@ -440,15 +480,13 @@ function isVersionCompatible(version: Labrinth.Versions.v2.Version): boolean {
|
||||
currentLoader: props.currentLoader,
|
||||
currentGameVersion: props.currentGameVersion,
|
||||
projectType: props.projectType,
|
||||
hasGameVersion,
|
||||
hasLoader,
|
||||
skipLoaderCheck,
|
||||
})
|
||||
}
|
||||
return compatible
|
||||
}
|
||||
|
||||
const currentVersion = computed(() => props.versions.find((v) => v.id === props.currentVersionId))
|
||||
const showUpdateWarning = computed(() => !isModpack.value)
|
||||
|
||||
const isDowngrade = computed(() => {
|
||||
if (!selectedVersion.value || !currentVersion.value) return false
|
||||
@@ -468,8 +506,13 @@ const filteredVersions = computed(() => {
|
||||
}
|
||||
|
||||
const beforeFilterCount = versions.length
|
||||
if (hideIncompatibleState.value) {
|
||||
versions = versions.filter(isVersionCompatible)
|
||||
if (!isModpack.value && hideIncompatibleState.value) {
|
||||
versions = versions.filter(
|
||||
(version) =>
|
||||
version.id === props.currentVersionId ||
|
||||
version.id === selectedVersion.value?.id ||
|
||||
isVersionCompatible(version),
|
||||
)
|
||||
}
|
||||
|
||||
debug('filteredVersions computed', {
|
||||
@@ -478,18 +521,23 @@ const filteredVersions = computed(() => {
|
||||
afterCompatibilityFilter: versions.length,
|
||||
hiddenByCompatibility: beforeFilterCount - versions.length,
|
||||
hideIncompatible: hideIncompatibleState.value,
|
||||
filteringCompatibility: !isModpack.value && hideIncompatibleState.value,
|
||||
})
|
||||
|
||||
return versions
|
||||
})
|
||||
|
||||
function shouldShowBadge(version: Labrinth.Versions.v2.Version): boolean {
|
||||
return version.id === props.currentVersionId || !isVersionCompatible(version)
|
||||
return version.id === props.currentVersionId || shouldShowIncompatibleBadge(version)
|
||||
}
|
||||
|
||||
function shouldShowIncompatibleBadge(version: Labrinth.Versions.v2.Version): boolean {
|
||||
return version.id !== props.currentVersionId && !isModpack.value && !isVersionCompatible(version)
|
||||
}
|
||||
|
||||
function getBadgeLabel(version: Labrinth.Versions.v2.Version): string {
|
||||
if (version.id === props.currentVersionId) return formatMessage(messages.currentBadge)
|
||||
if (!isVersionCompatible(version)) return formatMessage(messages.incompatibleBadge)
|
||||
if (shouldShowIncompatibleBadge(version)) return formatMessage(messages.incompatibleBadge)
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -499,8 +547,7 @@ function getBadgeClasses(version: Labrinth.Versions.v2.Version): string {
|
||||
return 'bg-surface-4 border-surface-5 text-primary'
|
||||
}
|
||||
|
||||
// Incompatible badge (takes precedence over version type)
|
||||
if (!isVersionCompatible(version)) {
|
||||
if (shouldShowIncompatibleBadge(version)) {
|
||||
return 'bg-highlight-orange border-brand-orange text-brand-orange'
|
||||
}
|
||||
|
||||
@@ -568,7 +615,61 @@ function handleVersionSelect(version: Labrinth.Versions.v2.Version) {
|
||||
|
||||
function handleUpdate(event: MouseEvent) {
|
||||
if (selectedVersion.value) {
|
||||
emit('update', selectedVersion.value, event)
|
||||
const changesGameVersion = versionChangesGameVersion(
|
||||
selectedVersion.value,
|
||||
props.currentGameVersion,
|
||||
)
|
||||
const shouldShowParentWarning =
|
||||
isModpack.value && !event.shiftKey && (changesGameVersion || isDowngrade.value)
|
||||
if (
|
||||
isModpack.value &&
|
||||
!event.shiftKey &&
|
||||
!isVersionCompatible(selectedVersion.value) &&
|
||||
!changesGameVersion
|
||||
) {
|
||||
pendingIncompatibleUpdate.value = {
|
||||
version: selectedVersion.value,
|
||||
event,
|
||||
}
|
||||
incompatibleUpdateModal.value?.show()
|
||||
return
|
||||
}
|
||||
|
||||
emitUpdate(selectedVersion.value, event, {
|
||||
hide: !shouldShowParentWarning,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function confirmIncompatibleUpdate() {
|
||||
const pendingUpdate = pendingIncompatibleUpdate.value
|
||||
pendingIncompatibleUpdate.value = null
|
||||
|
||||
if (pendingUpdate) {
|
||||
const current = currentVersion.value
|
||||
const isPendingDowngrade = current
|
||||
? new Date(pendingUpdate.version.date_published) < new Date(current.date_published)
|
||||
: false
|
||||
const changesGameVersion = versionChangesGameVersion(
|
||||
pendingUpdate.version,
|
||||
props.currentGameVersion,
|
||||
)
|
||||
const shouldShowParentWarning =
|
||||
isModpack.value && !pendingUpdate.event.shiftKey && (changesGameVersion || isPendingDowngrade)
|
||||
|
||||
emitUpdate(pendingUpdate.version, pendingUpdate.event, {
|
||||
hide: !shouldShowParentWarning,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emitUpdate(
|
||||
version: Labrinth.Versions.v2.Version,
|
||||
event: MouseEvent,
|
||||
options: { hide?: boolean } = {},
|
||||
) {
|
||||
emit('update', version, event)
|
||||
if (options.hide ?? true) {
|
||||
hide()
|
||||
}
|
||||
}
|
||||
@@ -580,7 +681,8 @@ function handleCancel() {
|
||||
|
||||
function show(initialVersionId?: string, options?: { switchMode?: boolean }) {
|
||||
searchQuery.value = ''
|
||||
hideIncompatibleState.value = true
|
||||
hideIncompatibleState.value = !isModpack.value
|
||||
pendingIncompatibleUpdate.value = null
|
||||
switchMode.value = options?.switchMode ?? false
|
||||
|
||||
debug('show() called', {
|
||||
|
||||
+17
-2
@@ -36,6 +36,7 @@ interface Props {
|
||||
modpackName?: string
|
||||
modpackIconUrl?: string
|
||||
enableToggle?: boolean
|
||||
busy?: boolean
|
||||
getOverflowOptions?: (item: ContentItem) => OverflowMenuOption[]
|
||||
switchVersion?: (item: ContentItem) => void
|
||||
}
|
||||
@@ -44,6 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
modpackName: undefined,
|
||||
modpackIconUrl: undefined,
|
||||
enableToggle: false,
|
||||
busy: false,
|
||||
getOverflowOptions: undefined,
|
||||
switchVersion: undefined,
|
||||
})
|
||||
@@ -247,12 +249,13 @@ const tableItems = computed<ContentCardTableItem[]>(() =>
|
||||
}
|
||||
: undefined,
|
||||
...(props.enableToggle ? { enabled: item.enabled } : {}),
|
||||
installing: item.installing === true,
|
||||
isClientOnly:
|
||||
isClientOnlyEnvironment(item.environment) ||
|
||||
!!item.pack_client_retained ||
|
||||
!!item.pack_client_depends,
|
||||
clientWarning: getClientWarningType(item),
|
||||
disabled: disabledIds.value.has(item.file_name),
|
||||
disabled: props.busy || disabledIds.value.has(item.file_name) || item.installing === true,
|
||||
overflowOptions: [
|
||||
...(props.switchVersion
|
||||
? [
|
||||
@@ -283,17 +286,20 @@ function getTypeIcon(type: string) {
|
||||
}
|
||||
|
||||
function handleEnabledChange(fileName: string, value: boolean) {
|
||||
if (props.busy) return
|
||||
const item = items.value.find((i) => i.file_name === fileName)
|
||||
if (!item) return
|
||||
emit('update:enabled', item, value)
|
||||
}
|
||||
|
||||
function bulkEnable() {
|
||||
if (props.busy) return
|
||||
emit('bulk:enable', [...selectedItems.value])
|
||||
selectedIds.value = []
|
||||
}
|
||||
|
||||
function bulkDisable() {
|
||||
if (props.busy) return
|
||||
emit('bulk:disable', [...selectedItems.value])
|
||||
selectedIds.value = []
|
||||
}
|
||||
@@ -361,7 +367,15 @@ function updateItem(fileName: string, updates: Partial<ContentItem> & { disabled
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ show, showLoading, hide, getState, restore, updateItem })
|
||||
function setItems(contentItems: ContentItem[]) {
|
||||
const contentFileNames = new Set(contentItems.map((item) => item.file_name))
|
||||
items.value = contentItems
|
||||
selectedIds.value = selectedIds.value.filter((id) => contentFileNames.has(id))
|
||||
disabledIds.value = new Set([...disabledIds.value].filter((id) => contentFileNames.has(id)))
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
defineExpose({ show, showLoading, hide, getState, restore, updateItem, setItems })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -544,6 +558,7 @@ defineExpose({ show, showLoading, hide, getState, restore, updateItem })
|
||||
<ContentSelectionBar
|
||||
v-if="props.enableToggle"
|
||||
:selected-items="selectedItems"
|
||||
:is-bulk-operating="props.busy"
|
||||
style="--left-bar-width: 0px; --right-bar-width: 0px"
|
||||
@clear="selectedIds = []"
|
||||
@enable="bulkEnable"
|
||||
|
||||
@@ -26,6 +26,7 @@ import ConfirmLeaveModal from '#ui/components/modal/ConfirmLeaveModal.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
import { formatLoaderLabel } from '#ui/utils/loaders'
|
||||
import { versionChangesGameVersion } from '#ui/utils/version-compatibility'
|
||||
|
||||
import ConfirmModpackUpdateModal from '../content-tab/components/modals/ConfirmModpackUpdateModal.vue'
|
||||
import ConfirmReinstallModal from '../content-tab/components/modals/ConfirmReinstallModal.vue'
|
||||
@@ -120,22 +121,31 @@ const isLocalFile = computed(() => {
|
||||
|
||||
function handleModpackUpdateRequest(version: Labrinth.Versions.v2.Version, event?: MouseEvent) {
|
||||
pendingUpdateVersion.value = version
|
||||
|
||||
const currentVersionId = ctx.updaterModalProps.value.currentVersionId
|
||||
const currentVersion = form.updatingProjectVersions.value.find((v) => v.id === currentVersionId)
|
||||
isUpdateDowngrade.value = currentVersion
|
||||
? new Date(version.date_published) < new Date(currentVersion.date_published)
|
||||
: false
|
||||
if (event?.shiftKey) {
|
||||
const shouldShowWarning =
|
||||
isUpdateDowngrade.value ||
|
||||
versionChangesGameVersion(version, ctx.updaterModalProps.value.currentGameVersion)
|
||||
|
||||
if (event?.shiftKey || !shouldShowWarning) {
|
||||
handleModpackUpdateConfirm()
|
||||
} else {
|
||||
modpackUpdateModal.value?.show()
|
||||
return
|
||||
}
|
||||
|
||||
modpackUpdateModal.value?.show()
|
||||
}
|
||||
|
||||
function handleModpackUpdateConfirm() {
|
||||
if (pendingUpdateVersion.value) {
|
||||
const version = pendingUpdateVersion.value
|
||||
if (version) {
|
||||
contentUpdaterModal.value?.hide()
|
||||
form.cancelEditing()
|
||||
form.handleUpdaterConfirm(pendingUpdateVersion.value)
|
||||
ctx.closeSettings?.()
|
||||
form.handleUpdaterConfirm(version)
|
||||
pendingUpdateVersion.value = null
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -64,6 +64,7 @@ export interface InstallationSettingsContext {
|
||||
reinstalling?: Ref<boolean>
|
||||
|
||||
afterSave?: () => Promise<void>
|
||||
closeSettings?: () => void
|
||||
|
||||
lockPlatform?: boolean
|
||||
hideLoaderVersion?: boolean
|
||||
|
||||
@@ -354,6 +354,7 @@ function toApiLoader(loader: string): Archon.Content.v1.Modloader {
|
||||
}
|
||||
|
||||
provideInstallationSettings({
|
||||
closeSettings: serverSettings.closeModal,
|
||||
onGameVersionHover: handleGameVersionHover,
|
||||
loading: computed(() => !server.value || addonsQuery.isLoading.value),
|
||||
installationInfo: computed(() => {
|
||||
|
||||
Reference in New Issue
Block a user