feat: clean up browse shared layout logic + introduce queuing (#6030)
* feat: clean up edge case behaviour and add queued to install logic * fix: remove version choice modal * feat: queued flow * feat: standardize headers in app on proj pages * fix: clear btn * feat: installing floating popup * fix: lint * fix: onboarding/reset logic change for modpacks * qa: big ol qa * fix: lint * fix: lint --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
:class="{
|
||||
'bg-brand border-button-border text-brand-inverted': modelValue,
|
||||
'bg-surface-2 border-surface-5 text-primary': !modelValue,
|
||||
'checkbox-shadow group-active:scale-95': disabled,
|
||||
'checkbox-shadow group-active:scale-95': !disabled,
|
||||
}"
|
||||
>
|
||||
<MinusIcon v-if="indeterminate" aria-hidden="true" stroke-width="3" />
|
||||
|
||||
@@ -8,6 +8,7 @@ const props = defineProps<{
|
||||
shown: boolean
|
||||
ariaLabel?: string
|
||||
belowModal?: boolean
|
||||
hideWhenModalOpen?: boolean
|
||||
}>()
|
||||
|
||||
const INTERCOM_BUBBLE_GAP = 8
|
||||
@@ -18,7 +19,7 @@ const compact = ref(false)
|
||||
|
||||
const { stackCount } = useModalStack()
|
||||
const pageContext = injectPageContext(null)
|
||||
const shown = computed(() => props.shown)
|
||||
const shown = computed(() => props.shown && (!props.hideWhenModalOpen || stackCount.value === 0))
|
||||
const intercomBubbleClearanceRequestId = Symbol('floating-action-bar')
|
||||
const zIndex = computed(() => 100 + stackCount.value * 10 + 8 + (!props.belowModal ? 1 : 0))
|
||||
const leftOffset = computed(
|
||||
@@ -82,11 +83,11 @@ function updateIntercomBubbleClearance() {
|
||||
)
|
||||
}
|
||||
|
||||
function updateBodyState(shown = props.shown) {
|
||||
function updateBodyState(isShown = shown.value) {
|
||||
if (typeof document === 'undefined') return
|
||||
|
||||
document.body.classList.toggle('floating-action-bar-shown', shown)
|
||||
if (!shown) {
|
||||
document.body.classList.toggle('floating-action-bar-shown', isShown)
|
||||
if (!isShown) {
|
||||
clearIntercomBubbleClearance()
|
||||
}
|
||||
}
|
||||
@@ -123,10 +124,10 @@ watch(
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.shown,
|
||||
async (shown) => {
|
||||
shown,
|
||||
async (isShown) => {
|
||||
await nextTick()
|
||||
updateBodyState(shown)
|
||||
updateBodyState(isShown)
|
||||
scheduleIntercomBubbleClearanceUpdate()
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -175,7 +176,7 @@ onUnmounted(() => {
|
||||
ref="toolbarEl"
|
||||
role="toolbar"
|
||||
:aria-label="ariaLabel"
|
||||
class="relative overflow-clip flex items-center gap-2 rounded-[20px] bg-surface-3 border border-surface-5 border-solid mx-auto max-w-[60vw] px-4 py-3 shadow-[0px_1px_3px_0px_rgba(0,0,0,0.3),0px_6px_10px_0px_rgba(0,0,0,0.15)]"
|
||||
class="relative overflow-clip flex items-center gap-1.5 rounded-[20px] bg-surface-3 border border-surface-5 border-solid mx-auto max-w-[60vw] px-3 py-2.5 shadow-[0px_1px_3px_0px_rgba(0,0,0,0.3),0px_6px_10px_0px_rgba(0,0,0,0.15)]"
|
||||
:class="{ 'bar-compact': compact }"
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -7,18 +7,13 @@
|
||||
:waiting="isWaiting"
|
||||
@dismiss="emit('dismiss')"
|
||||
>
|
||||
<template #icon>
|
||||
<slot v-if="!contentError" name="icon">
|
||||
<SpinnerIcon class="h-6 w-6 flex-none animate-spin text-brand-blue" />
|
||||
</slot>
|
||||
</template>
|
||||
<template #header>
|
||||
{{ contentError ? 'Installation failed' : "We're preparing your server" }}
|
||||
{{ headerLabel }}
|
||||
</template>
|
||||
<template v-if="contentError">
|
||||
{{ errorLabel }}
|
||||
</template>
|
||||
<template v-else-if="progress">{{ phaseLabel }}</template>
|
||||
<template v-else-if="effectivePhase">{{ phaseLabel }}</template>
|
||||
<div v-else class="ticker-container">
|
||||
<div class="ticker-content">
|
||||
<div
|
||||
@@ -35,7 +30,7 @@
|
||||
<ButtonStyled color="red" type="outlined">
|
||||
<button class="!border" type="button" @click="emit('retry')">
|
||||
<RotateCounterClockwiseIcon class="size-5" />
|
||||
Retry
|
||||
{{ formatMessage(commonMessages.retryButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
@@ -44,9 +39,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RotateCounterClockwiseIcon } from '@modrinth/assets'
|
||||
import SpinnerIcon from '@modrinth/assets/icons/spinner.svg'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
import Admonition from '../base/Admonition.vue'
|
||||
import ButtonStyled from '../base/ButtonStyled.vue'
|
||||
|
||||
@@ -62,6 +59,7 @@ export interface ContentError {
|
||||
|
||||
const props = defineProps<{
|
||||
progress?: SyncProgress | null
|
||||
fallbackPhase?: SyncProgress['phase'] | null
|
||||
contentError?: ContentError | null
|
||||
dismissible?: boolean
|
||||
}>()
|
||||
@@ -71,44 +69,123 @@ const emit = defineEmits<{
|
||||
dismiss: []
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
errorHeader: {
|
||||
id: 'servers.installing-banner.error.header',
|
||||
defaultMessage: 'Installation failed',
|
||||
},
|
||||
preparingHeader: {
|
||||
id: 'servers.installing-banner.preparing.header',
|
||||
defaultMessage: "We're preparing your server",
|
||||
},
|
||||
invalidLoaderVersionError: {
|
||||
id: 'servers.installing-banner.error.invalid-loader-version',
|
||||
defaultMessage:
|
||||
'The specified loader or Minecraft version could not be installed. It may be invalid or unsupported.',
|
||||
},
|
||||
unsupportedLoaderVersionError: {
|
||||
id: 'servers.installing-banner.error.unsupported-loader-version',
|
||||
defaultMessage: 'This version of Minecraft or loader is not yet supported by Modrinth Hosting.',
|
||||
},
|
||||
internalPlatformError: {
|
||||
id: 'servers.installing-banner.error.internal-platform',
|
||||
defaultMessage: 'An internal error occurred while installing the platform. Please try again.',
|
||||
},
|
||||
noPrimaryFileError: {
|
||||
id: 'servers.installing-banner.error.no-primary-file',
|
||||
defaultMessage:
|
||||
'This modpack version does not include a downloadable file. It may have been packaged incorrectly.',
|
||||
},
|
||||
modpackInstallFailedError: {
|
||||
id: 'servers.installing-banner.error.modpack-install-failed',
|
||||
defaultMessage: 'The modpack could not be installed. It may be corrupted or incompatible.',
|
||||
},
|
||||
unknownError: {
|
||||
id: 'servers.installing-banner.error.unknown',
|
||||
defaultMessage: 'An unexpected error occurred during installation.',
|
||||
},
|
||||
installingPlatform: {
|
||||
id: 'servers.installing-banner.phase.installing-platform',
|
||||
defaultMessage: 'Installing platform...',
|
||||
},
|
||||
installingModpack: {
|
||||
id: 'servers.installing-banner.phase.installing-modpack',
|
||||
defaultMessage: 'Installing modpack...',
|
||||
},
|
||||
installingAddons: {
|
||||
id: 'servers.installing-banner.phase.installing-addons',
|
||||
defaultMessage: 'Installing addons...',
|
||||
},
|
||||
tickerOrganizingFiles: {
|
||||
id: 'servers.installing-banner.ticker.organizing-files',
|
||||
defaultMessage: 'Organizing files...',
|
||||
},
|
||||
tickerDownloadingMods: {
|
||||
id: 'servers.installing-banner.ticker.downloading-mods',
|
||||
defaultMessage: 'Downloading mods...',
|
||||
},
|
||||
tickerConfiguringServer: {
|
||||
id: 'servers.installing-banner.ticker.configuring-server',
|
||||
defaultMessage: 'Configuring server...',
|
||||
},
|
||||
tickerSettingUpEnvironment: {
|
||||
id: 'servers.installing-banner.ticker.setting-up-environment',
|
||||
defaultMessage: 'Setting up environment...',
|
||||
},
|
||||
tickerAddingJava: {
|
||||
id: 'servers.installing-banner.ticker.adding-java',
|
||||
defaultMessage: 'Adding Java...',
|
||||
},
|
||||
})
|
||||
|
||||
const errorLabel = computed(() => {
|
||||
const desc = props.contentError?.description?.toLowerCase()
|
||||
const step = props.contentError?.step
|
||||
|
||||
if (step === 'modloader') {
|
||||
if (desc === 'the specified version may be incorrect') {
|
||||
return 'The specified loader or Minecraft version could not be installed. It may be invalid or unsupported.'
|
||||
return formatMessage(messages.invalidLoaderVersionError)
|
||||
}
|
||||
if (desc === 'this version is not yet supported') {
|
||||
return 'This version of Minecraft or loader is not yet supported by Modrinth Hosting.'
|
||||
return formatMessage(messages.unsupportedLoaderVersionError)
|
||||
}
|
||||
if (desc === 'internal error') {
|
||||
return 'An internal error occurred while installing the platform. Please try again.'
|
||||
return formatMessage(messages.internalPlatformError)
|
||||
}
|
||||
}
|
||||
|
||||
if (step === 'modpack') {
|
||||
if (desc?.includes('no primary file')) {
|
||||
return 'This modpack version does not include a downloadable file. It may have been packaged incorrectly.'
|
||||
return formatMessage(messages.noPrimaryFileError)
|
||||
}
|
||||
if (desc?.includes('failed to install')) {
|
||||
return 'The modpack could not be installed. It may be corrupted or incompatible.'
|
||||
return formatMessage(messages.modpackInstallFailedError)
|
||||
}
|
||||
}
|
||||
|
||||
return props.contentError?.description ?? 'An unexpected error occurred during installation.'
|
||||
return props.contentError?.description ?? formatMessage(messages.unknownError)
|
||||
})
|
||||
|
||||
const effectivePhase = computed(() => props.progress?.phase ?? props.fallbackPhase ?? null)
|
||||
|
||||
const headerLabel = computed(() => {
|
||||
if (props.contentError) return formatMessage(messages.errorHeader)
|
||||
if (effectivePhase.value === 'Addons') return formatMessage(commonMessages.installingContentLabel)
|
||||
return formatMessage(messages.preparingHeader)
|
||||
})
|
||||
|
||||
const phaseLabel = computed(() => {
|
||||
switch (props.progress?.phase) {
|
||||
switch (effectivePhase.value) {
|
||||
case 'InstallingLoader':
|
||||
return 'Installing platform...'
|
||||
return formatMessage(messages.installingPlatform)
|
||||
case 'InstallingPack':
|
||||
return 'Installing modpack...'
|
||||
return formatMessage(messages.installingModpack)
|
||||
case 'Addons':
|
||||
return 'Installing addons...'
|
||||
return formatMessage(messages.installingAddons)
|
||||
default:
|
||||
return 'Installing...'
|
||||
return formatMessage(commonMessages.installingLabel)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -122,13 +199,13 @@ const isWaiting = computed(() => {
|
||||
return !props.progress || props.progress.percent <= 0
|
||||
})
|
||||
|
||||
const tickerMessages = [
|
||||
'Organizing files...',
|
||||
'Downloading mods...',
|
||||
'Configuring server...',
|
||||
'Setting up environment...',
|
||||
'Adding Java...',
|
||||
]
|
||||
const tickerMessages = computed(() => [
|
||||
formatMessage(messages.tickerOrganizingFiles),
|
||||
formatMessage(messages.tickerDownloadingMods),
|
||||
formatMessage(messages.tickerConfiguringServer),
|
||||
formatMessage(messages.tickerSettingUpEnvironment),
|
||||
formatMessage(messages.tickerAddingJava),
|
||||
])
|
||||
|
||||
const currentIndex = ref(0)
|
||||
|
||||
@@ -136,7 +213,7 @@ let intervalId: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
onMounted(() => {
|
||||
intervalId = setInterval(() => {
|
||||
currentIndex.value = (currentIndex.value + 1) % tickerMessages.length
|
||||
currentIndex.value = (currentIndex.value + 1) % tickerMessages.value.length
|
||||
}, 3000)
|
||||
})
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import Admonition from '#ui/components/base/Admonition.vue'
|
||||
import StackedAdmonitions, {
|
||||
type StackedAdmonitionItem,
|
||||
} from '#ui/components/base/StackedAdmonitions.vue'
|
||||
import { ServerIcon } from '#ui/components/servers/icons'
|
||||
import InstallingBanner, {
|
||||
type ContentError,
|
||||
type SyncProgress,
|
||||
@@ -23,7 +22,6 @@ import UploadAdmonition from './UploadAdmonition.vue'
|
||||
const props = defineProps<{
|
||||
syncProgress?: SyncProgress | null
|
||||
contentError?: ContentError | null
|
||||
serverImage?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -59,7 +57,13 @@ const isOnContentTab = computed(() => route.path.includes('/content'))
|
||||
const isOnFilesTab = computed(() => route.path.includes('/files'))
|
||||
|
||||
const bannerCoversInstalling = computed(
|
||||
() => ctx.server.value?.status === 'installing' || ctx.isSyncingContent.value,
|
||||
() =>
|
||||
ctx.server.value?.status === 'installing' ||
|
||||
ctx.isSyncingContent.value ||
|
||||
ctx.busyReasons.value.some(
|
||||
(r) =>
|
||||
r.reason.id === 'servers.busy.installing' || r.reason.id === 'servers.busy.syncing-content',
|
||||
),
|
||||
)
|
||||
|
||||
function isBackupReason(id: string) {
|
||||
@@ -165,8 +169,7 @@ type ServerAdmonitionItem = StackedAdmonitionItem & {
|
||||
|
||||
const showInstallingBanner = computed(() => {
|
||||
if (!ctx.server.value) return false
|
||||
const installing =
|
||||
ctx.server.value.status === 'installing' || ctx.isSyncingContent.value || !!props.contentError
|
||||
const installing = bannerCoversInstalling.value || !!props.contentError
|
||||
if (!installing) return false
|
||||
if (contentErrorKey.value && dismissedContentErrorKey.value === contentErrorKey.value)
|
||||
return false
|
||||
@@ -366,15 +369,12 @@ function onContentErrorDismiss() {
|
||||
<InstallingBanner
|
||||
v-if="item.kind === 'installing'"
|
||||
:progress="syncProgress"
|
||||
:fallback-phase="isOnContentTab && !syncProgress ? 'Addons' : null"
|
||||
:content-error="contentError"
|
||||
:dismissible="dismissible && !!contentError"
|
||||
@dismiss="onContentErrorDismiss"
|
||||
@retry="emit('content-retry')"
|
||||
>
|
||||
<template #icon>
|
||||
<ServerIcon :image="serverImage" class="!h-6 !w-6" />
|
||||
</template>
|
||||
</InstallingBanner>
|
||||
/>
|
||||
<UploadAdmonition v-else-if="item.kind === 'upload'" />
|
||||
<FileOperationAdmonition
|
||||
v-else-if="item.kind === 'fs-op'"
|
||||
|
||||
@@ -12,10 +12,20 @@ export type PowerAction = 'Start' | 'Stop' | 'Restart' | 'Kill'
|
||||
export function useServerPowerAction(options?: { disabled?: Ref<boolean> }) {
|
||||
const { formatMessage } = useVIntl()
|
||||
const client = injectModrinthClient()
|
||||
const { serverId, server, powerState, busyReasons } = injectModrinthServerContext()
|
||||
const { serverId, server, powerState, isSyncingContent, busyReasons } =
|
||||
injectModrinthServerContext()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const isInstalling = computed(() => server.value.status === 'installing')
|
||||
const isInstalling = computed(
|
||||
() =>
|
||||
server.value.status === 'installing' ||
|
||||
isSyncingContent.value ||
|
||||
busyReasons.value.some(
|
||||
(r) =>
|
||||
r.reason.id === 'servers.busy.installing' ||
|
||||
r.reason.id === 'servers.busy.syncing-content',
|
||||
),
|
||||
)
|
||||
const isRunning = computed(() => powerState.value === 'running')
|
||||
const isStopping = computed(() => powerState.value === 'stopping')
|
||||
const isStarting = computed(() => powerState.value === 'starting')
|
||||
|
||||
Reference in New Issue
Block a user