You've already forked AstralRinth
feat: backups page cleanup before worlds (#5844)
* feat: card alignment + fix modals * feat: change admon title in restore alert modal * fix: lint * feat: backups queue api into api-client * feat: impl backup queue api endpoints into frontend * feat: ack fix * feat: bulk actions * feat: bulk delete impl * fix: lint * fix: align error states * fix: transition group * feat: ready for qa * fix: lint * feat: qa * feat: stacked admonitions component * fix: issues with stacking * feat: hook up admonition stacking + fix app csp for staging kyros nodes * fix: logs.vue * qa: close stack on admonitions click * fix: all problems with stacked admonitions * qa: admonition cleanup and copy overhaul draft * fix: qa issues padding * fix: padding bug * feat: qa * fix: intercom in app csp bug * fix: positioning intercom * feat: loading overlay on top of console + admon consistency changes * feat: scroll indicator fade in backup delete modal + admon timestamp fix * feat: move action bar behind modal * fix: lint + i18n * fix: server ping spam on filter (cache but clear on unmount) * fix: 1 admon fade in flicker issue * chore: temp staging undo * qa: changes * fix: lint * chore: revert staging to use staging * fix: scoping
This commit is contained in:
@@ -100,7 +100,13 @@ export function useBrowseSearch(options: UseBrowseSearchOptions): BrowseSearchSt
|
||||
serverFilterTypes,
|
||||
serverRequestParams,
|
||||
createServerPageParams,
|
||||
} = useServerSearch({ tags: options.tags, query, maxResults, currentPage })
|
||||
} = useServerSearch({
|
||||
tags: options.tags,
|
||||
query,
|
||||
maxResults,
|
||||
currentPage,
|
||||
providedFilters: options.providedFilters,
|
||||
})
|
||||
|
||||
const effectiveRequestParams = computed(() =>
|
||||
isServerType.value ? serverRequestParams.value : requestParams.value,
|
||||
|
||||
@@ -174,7 +174,7 @@ const maxResultsOptions = computed<ComboboxOption<number>[]>(() =>
|
||||
:disabled="action.disabled"
|
||||
@click.stop="action.onClick"
|
||||
>
|
||||
<component :is="action.icon" />
|
||||
<component :is="action.icon" :class="action.iconClass" />
|
||||
<template v-if="!action.circular">{{ action.label }}</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -241,7 +241,7 @@ const maxResultsOptions = computed<ComboboxOption<number>[]>(() =>
|
||||
:disabled="action.disabled"
|
||||
@click.stop="action.onClick"
|
||||
>
|
||||
<component :is="action.icon" />
|
||||
<component :is="action.icon" :class="action.iconClass" />
|
||||
<template v-if="!action.circular">{{ action.label }}</template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
@@ -98,7 +98,7 @@ function getFilterOpenByDefault(filterId: string): boolean {
|
||||
>
|
||||
<Checkbox
|
||||
v-model="ctx.hideInstalled!.value"
|
||||
:label="ctx.hideInstalledLabel?.value ?? 'Hide installed content'"
|
||||
:label="ctx.hideInstalledLabel?.value ?? 'Hide already installed content'"
|
||||
class="filter-checkbox"
|
||||
@update:model-value="ctx.onFilterChange()"
|
||||
@click.prevent.stop
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface CardAction {
|
||||
key: string
|
||||
label: string
|
||||
icon: Component
|
||||
iconClass?: string
|
||||
disabled?: boolean
|
||||
color?: 'brand' | 'red'
|
||||
type?: 'standard' | 'outlined' | 'transparent'
|
||||
|
||||
@@ -58,9 +58,10 @@
|
||||
ref="terminalRef"
|
||||
class="min-h-0 flex-1"
|
||||
:show-input="resolvedShowInput"
|
||||
:disable-input="resolvedDisableInput"
|
||||
:disable-input="resolvedInputDisabled"
|
||||
:fullscreen="isFullscreen"
|
||||
:empty-state-type="ctx.emptyStateType"
|
||||
:loading="resolvedLoading"
|
||||
@command="handleCommand"
|
||||
@ready="handleTerminalReady"
|
||||
/>
|
||||
@@ -206,6 +207,15 @@ const resolvedDisableInput = computed(() => {
|
||||
return isRef(v) ? v.value : v
|
||||
})
|
||||
|
||||
// needs historical log start/end flags on ws to be properly useful
|
||||
const resolvedLoading = computed(() => {
|
||||
const v = ctx.loading
|
||||
if (!v) return false
|
||||
return v.value
|
||||
})
|
||||
|
||||
const resolvedInputDisabled = computed(() => resolvedDisableInput.value || resolvedLoading.value)
|
||||
|
||||
const resolvedShareDisabled = computed(() => {
|
||||
const v = ctx.shareDisabled
|
||||
if (!v) return false
|
||||
@@ -237,6 +247,11 @@ function rewriteFiltered() {
|
||||
const term = terminalRef.value?.terminal
|
||||
if (!term) return
|
||||
const lines = ctx.logLines.value
|
||||
if (resolvedLoading.value && lines.length === 0 && isLiveSource.value) {
|
||||
terminalRef.value?.clearEmptyState()
|
||||
lastWrittenIndex = 0
|
||||
return
|
||||
}
|
||||
if (lines.length === 0 && isLiveSource.value) {
|
||||
writeEmptyState()
|
||||
return
|
||||
@@ -271,6 +286,12 @@ watch(ctx.logLines, (lines, oldLines) => {
|
||||
if (!term) return
|
||||
|
||||
if (lines.length === 0 && isLiveSource.value) {
|
||||
if (resolvedLoading.value) {
|
||||
terminalRef.value?.clearEmptyState()
|
||||
lastWrittenIndex = 0
|
||||
return
|
||||
}
|
||||
|
||||
writeEmptyState()
|
||||
return
|
||||
}
|
||||
@@ -312,6 +333,12 @@ watch(searchQuery, () => {
|
||||
}, 200)
|
||||
})
|
||||
|
||||
watch(resolvedLoading, (loading) => {
|
||||
if (!loading) {
|
||||
rewriteFiltered()
|
||||
}
|
||||
})
|
||||
|
||||
function handleCommand(cmd: string) {
|
||||
ctx.sendCommand?.(cmd)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
|
||||
import { useServerBackupsQueue } from '#ui/composables/server-backups-queue'
|
||||
import {
|
||||
injectAppBackup,
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
injectNotificationManager,
|
||||
} from '#ui/providers/'
|
||||
} from '#ui/providers'
|
||||
|
||||
export function useInlineBackup(backupName: string | (() => string)) {
|
||||
const serverCtx = injectModrinthServerContext(null)
|
||||
@@ -60,110 +61,65 @@ export function useInlineBackup(backupName: string | (() => string)) {
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { serverId, worldId, backupsState, markBackupCancelled } = serverCtx
|
||||
const { serverId, worldId } = serverCtx
|
||||
|
||||
const isBackingUp = ref(false)
|
||||
const { activeOperationByBackupId, backups, hasActiveCreate, invalidate } = useServerBackupsQueue(
|
||||
computed(() => serverId),
|
||||
worldId,
|
||||
)
|
||||
|
||||
const createdBackupId = ref<string | null>(null)
|
||||
const pendingCreate = ref(false)
|
||||
const backupFailed = ref(false)
|
||||
const backupComplete = ref(false)
|
||||
const backupCancelled = ref(false)
|
||||
const isCancelling = ref(false)
|
||||
const createdBackupId = ref<string | null>(null)
|
||||
|
||||
const externalBackupInProgress = computed(() => {
|
||||
for (const [id, entry] of backupsState.entries()) {
|
||||
if (id !== createdBackupId.value && entry.create?.state === 'ongoing') return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Watch backupsState for websocket progress events from Kyros
|
||||
watch(
|
||||
() => {
|
||||
if (!createdBackupId.value) return null
|
||||
return backupsState.get(createdBackupId.value)
|
||||
},
|
||||
(entry) => {
|
||||
if (!entry?.create) return
|
||||
|
||||
if (entry.create.state === 'done') {
|
||||
stopPolling()
|
||||
isBackingUp.value = false
|
||||
backupComplete.value = true
|
||||
} else if (entry.create.state === 'cancelled') {
|
||||
stopPolling()
|
||||
isBackingUp.value = false
|
||||
isCancelling.value = false
|
||||
backupCancelled.value = true
|
||||
} else if (entry.create.state === 'failed') {
|
||||
stopPolling()
|
||||
isBackingUp.value = false
|
||||
backupFailed.value = true
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
const myBackup = computed(() =>
|
||||
createdBackupId.value ? backups.value.find((b) => b.id === createdBackupId.value) : undefined,
|
||||
)
|
||||
const myActiveOp = computed(() =>
|
||||
createdBackupId.value ? activeOperationByBackupId.value.get(createdBackupId.value) : undefined,
|
||||
)
|
||||
|
||||
// Fallback: poll the REST API in case websocket events don't arrive
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
const isBackingUp = computed(
|
||||
() =>
|
||||
!backupComplete.value &&
|
||||
!backupFailed.value &&
|
||||
!backupCancelled.value &&
|
||||
(!!createdBackupId.value || pendingCreate.value),
|
||||
)
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer !== null) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
const externalBackupInProgress = computed(() => hasActiveCreate.value && !myActiveOp.value)
|
||||
|
||||
async function pollBackupStatus(backupId: string) {
|
||||
if (!isBackingUp.value) {
|
||||
stopPolling()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const backup = await client.archon.backups_v1.get(serverId, worldId.value!, backupId)
|
||||
const isTerminal =
|
||||
backup.status === 'done' || backup.status === 'error' || backup.status === 'timed_out'
|
||||
|
||||
if (isTerminal) {
|
||||
stopPolling()
|
||||
if (!isBackingUp.value) return
|
||||
if (backup.status === 'error' || backup.status === 'timed_out') {
|
||||
isBackingUp.value = false
|
||||
backupFailed.value = true
|
||||
} else {
|
||||
isBackingUp.value = false
|
||||
backupComplete.value = true
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
stopPolling()
|
||||
isBackingUp.value = false
|
||||
backupFailed.value = true
|
||||
}
|
||||
}
|
||||
watch(
|
||||
myBackup,
|
||||
(b) => {
|
||||
if (!createdBackupId.value || !b) return
|
||||
if (b.status === 'done') backupComplete.value = true
|
||||
else if (b.status === 'error' || b.status === 'timed_out') backupFailed.value = true
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function startBackup() {
|
||||
if (!worldId.value) return
|
||||
|
||||
const name = typeof backupName === 'function' ? backupName() : backupName
|
||||
|
||||
isBackingUp.value = true
|
||||
backupFailed.value = false
|
||||
backupComplete.value = false
|
||||
backupCancelled.value = false
|
||||
isCancelling.value = false
|
||||
createdBackupId.value = null
|
||||
pendingCreate.value = true
|
||||
|
||||
try {
|
||||
const { id } = await client.archon.backups_v1.create(serverId, worldId.value, { name })
|
||||
const { id } = await client.archon.backups_queue_v1.create(serverId, worldId.value, { name })
|
||||
createdBackupId.value = id
|
||||
|
||||
stopPolling()
|
||||
pollTimer = setInterval(() => pollBackupStatus(id), 3000)
|
||||
await invalidate()
|
||||
} catch (error) {
|
||||
isBackingUp.value = false
|
||||
backupFailed.value = true
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
const isRateLimit = message.includes('429')
|
||||
addNotification({
|
||||
@@ -171,6 +127,8 @@ export function useInlineBackup(backupName: string | (() => string)) {
|
||||
title: 'Error creating backup',
|
||||
text: isRateLimit ? "You're creating backups too fast." : message,
|
||||
})
|
||||
} finally {
|
||||
pendingCreate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,23 +136,19 @@ export function useInlineBackup(backupName: string | (() => string)) {
|
||||
if (!worldId.value || !createdBackupId.value || !isBackingUp.value) return
|
||||
|
||||
isCancelling.value = true
|
||||
stopPolling()
|
||||
markBackupCancelled(createdBackupId.value)
|
||||
|
||||
try {
|
||||
await client.archon.backups_v1.delete(serverId, worldId.value, createdBackupId.value)
|
||||
isBackingUp.value = false
|
||||
isCancelling.value = false
|
||||
backupCancelled.value = true
|
||||
isCancelling.value = false
|
||||
await invalidate()
|
||||
addNotification({
|
||||
type: 'info',
|
||||
title: 'Backup cancelled',
|
||||
text: 'The backup has been cancelled. You can create a new one or proceed without a backup.',
|
||||
})
|
||||
} catch {
|
||||
isBackingUp.value = false
|
||||
isCancelling.value = false
|
||||
backupFailed.value = true
|
||||
isCancelling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +170,6 @@ export function useInlineBackup(backupName: string | (() => string)) {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
|
||||
@@ -17,16 +17,12 @@ import {
|
||||
ShareIcon,
|
||||
TextCursorInputIcon,
|
||||
TrashIcon,
|
||||
UploadIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { formatBytes } from '@modrinth/utils'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import Admonition from '#ui/components/base/Admonition.vue'
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import EmptyState from '#ui/components/base/EmptyState.vue'
|
||||
import OverflowMenu from '#ui/components/base/OverflowMenu.vue'
|
||||
import ProgressBar from '#ui/components/base/ProgressBar.vue'
|
||||
import StyledInput from '#ui/components/base/StyledInput.vue'
|
||||
import { useDebugLogger } from '#ui/composables/debug-logger'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
@@ -53,6 +49,15 @@ import type { ContentCardTableItem, ContentItem } from './types'
|
||||
const { formatMessage } = useVIntl()
|
||||
const debug = useDebugLogger('ContentPageLayout')
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
bottomPadding?: boolean
|
||||
}>(),
|
||||
{
|
||||
bottomPadding: true,
|
||||
},
|
||||
)
|
||||
|
||||
const messages = defineMessages({
|
||||
loadingContent: {
|
||||
id: 'content.page-layout.loading',
|
||||
@@ -134,18 +139,10 @@ const messages = defineMessages({
|
||||
id: 'content.page-layout.share.label',
|
||||
defaultMessage: 'Share',
|
||||
},
|
||||
uploadingFiles: {
|
||||
id: 'content.page-layout.uploading-files',
|
||||
defaultMessage: 'Uploading files ({completed}/{total})',
|
||||
},
|
||||
sortByLabel: {
|
||||
id: 'content.page-layout.sort.label',
|
||||
defaultMessage: 'Sort by {mode}',
|
||||
},
|
||||
busyDescription: {
|
||||
id: 'content.page-layout.busy-description',
|
||||
defaultMessage: 'Please wait for the operation to complete before editing content.',
|
||||
},
|
||||
pleaseWait: {
|
||||
id: 'content.page-layout.please-wait',
|
||||
defaultMessage: 'Please wait',
|
||||
@@ -154,12 +151,6 @@ const messages = defineMessages({
|
||||
|
||||
const ctx = injectContentManager()
|
||||
|
||||
const uploadOverallProgress = computed(() => {
|
||||
const state = ctx.uploadState?.value
|
||||
if (!state || !state.isUploading || state.totalFiles === 0) return 0
|
||||
return Math.min((state.completedFiles + state.currentFileProgress) / state.totalFiles, 1)
|
||||
})
|
||||
|
||||
type SortMode = 'alphabetical-asc' | 'alphabetical-desc' | 'date-added-newest' | 'date-added-oldest'
|
||||
const sortMode = ref<SortMode>('alphabetical-asc')
|
||||
|
||||
@@ -502,7 +493,7 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 pb-6">
|
||||
<div class="flex flex-col gap-4" :class="{ 'pb-6': props.bottomPadding }">
|
||||
<template v-if="!ctx.loading.value">
|
||||
<div
|
||||
v-if="ctx.error.value"
|
||||
@@ -518,11 +509,6 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<Admonition v-if="ctx.isBusy.value && ctx.busyMessage?.value" type="warning">
|
||||
<template #header>{{ ctx.busyMessage.value }}</template>
|
||||
{{ formatMessage(messages.busyDescription) }}
|
||||
</Admonition>
|
||||
|
||||
<ContentModpackCard
|
||||
v-if="ctx.modpack.value"
|
||||
:project="ctx.modpack.value.project"
|
||||
@@ -550,43 +536,6 @@ const confirmUnlinkModal = ref<InstanceType<typeof ConfirmUnlinkModal>>()
|
||||
@dismiss-content-hint="ctx.dismissContentHint?.()"
|
||||
/>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out overflow-hidden"
|
||||
enter-from-class="opacity-0 max-h-0"
|
||||
enter-to-class="opacity-100 max-h-40"
|
||||
leave-active-class="transition-all duration-200 ease-in overflow-hidden"
|
||||
leave-from-class="opacity-100 max-h-40"
|
||||
leave-to-class="opacity-0 max-h-0"
|
||||
aria-live="polite"
|
||||
>
|
||||
<Admonition
|
||||
v-if="ctx.uploadState?.value?.isUploading"
|
||||
type="info"
|
||||
show-actions-underneath
|
||||
>
|
||||
<template #icon>
|
||||
<UploadIcon class="h-6 w-6 flex-none text-brand-blue" />
|
||||
</template>
|
||||
<template #header>
|
||||
{{
|
||||
formatMessage(messages.uploadingFiles, {
|
||||
completed: ctx.uploadState?.value?.completedFiles ?? 0,
|
||||
total: ctx.uploadState?.value?.totalFiles ?? 0,
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<span class="text-secondary">
|
||||
{{ formatBytes(ctx.uploadState?.value?.uploadedBytes ?? 0) }}
|
||||
/ {{ formatBytes(ctx.uploadState?.value?.totalBytes ?? 0) }} ({{
|
||||
Math.round(uploadOverallProgress * 100)
|
||||
}}%)
|
||||
</span>
|
||||
<template #actions>
|
||||
<ProgressBar :progress="uploadOverallProgress" :max="1" color="blue" full-width />
|
||||
</template>
|
||||
</Admonition>
|
||||
</Transition>
|
||||
|
||||
<template v-if="ctx.items.value.length > 0">
|
||||
<div class="flex flex-col gap-4">
|
||||
<span v-if="ctx.modpack.value" class="text-xl font-semibold text-contrast">
|
||||
|
||||
@@ -25,8 +25,6 @@ export interface ContentModpackData {
|
||||
disabledText?: string
|
||||
}
|
||||
|
||||
export type { UploadState } from '@modrinth/api-client'
|
||||
|
||||
export interface ContentManagerContext {
|
||||
// Data
|
||||
items: Ref<ContentItem[]> | ComputedRef<ContentItem[]>
|
||||
@@ -79,9 +77,6 @@ export interface ContentManagerContext {
|
||||
// Share support (optional — when undefined, share button becomes hidden entirely)
|
||||
shareItems?: (items: ContentItem[], format: 'names' | 'file-names' | 'urls' | 'markdown') => void
|
||||
|
||||
// Upload progress (optional)
|
||||
uploadState?: Ref<UploadState> | ComputedRef<UploadState>
|
||||
|
||||
// Bulk operation guard — set by layout, checked by providers to suppress refreshes
|
||||
isBulkOperating?: Ref<boolean>
|
||||
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
<template>
|
||||
<TransitionGroup
|
||||
name="fs-op"
|
||||
enter-active-class="transition-all duration-300 ease-out overflow-hidden"
|
||||
enter-from-class="opacity-0 max-h-0"
|
||||
enter-to-class="opacity-100 max-h-40"
|
||||
leave-active-class="transition-all duration-200 ease-in overflow-hidden"
|
||||
leave-from-class="opacity-100 max-h-40"
|
||||
leave-to-class="opacity-0 max-h-0"
|
||||
>
|
||||
<Admonition
|
||||
v-for="op in activeOperations"
|
||||
:key="`fs-op-${op.op}-${op.src}`"
|
||||
:type="op.state === 'done' ? 'success' : op.state?.startsWith('fail') ? 'critical' : 'info'"
|
||||
class="mb-4"
|
||||
>
|
||||
<template #icon="{ iconClass }">
|
||||
<PackageOpenIcon :class="iconClass" />
|
||||
</template>
|
||||
<template #header>
|
||||
{{
|
||||
formatMessage(messages.extracting, {
|
||||
source: op.src.includes('https://') ? formatMessage(messages.modpackFromUrl) : op.src,
|
||||
})
|
||||
}}
|
||||
<span v-if="op.state === 'done'" class="font-normal text-green">
|
||||
— {{ formatMessage(commonMessages.doneLabel) }}</span
|
||||
>
|
||||
<span v-else-if="op.state?.startsWith('fail')" class="font-normal text-red">
|
||||
— {{ formatMessage(messages.failed) }}</span
|
||||
>
|
||||
</template>
|
||||
<span class="text-secondary">
|
||||
{{
|
||||
formatMessage(messages.extracted, {
|
||||
size: 'bytes_processed' in op ? formatBytes(op.bytes_processed ?? 0) : '0 B',
|
||||
})
|
||||
}}
|
||||
<template v-if="'current_file' in op && op.current_file">
|
||||
— {{ op.current_file?.split('/')?.pop() }}
|
||||
</template>
|
||||
</span>
|
||||
<template v-if="op.id" #top-right-actions>
|
||||
<ButtonStyled
|
||||
v-if="op.state !== 'done' && !op.state?.startsWith('fail')"
|
||||
type="outlined"
|
||||
color="blue"
|
||||
>
|
||||
<button class="!border" @click="ctx.dismissOperation(op.id!, 'cancel')">
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-if="op.state === 'done' || op.state?.startsWith('fail')"
|
||||
circular
|
||||
type="transparent"
|
||||
hover-color-fill="background"
|
||||
:color="op.state === 'done' ? 'green' : 'red'"
|
||||
>
|
||||
<button @click="ctx.dismissOperation(op.id!, 'dismiss')">
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template #progress>
|
||||
<ProgressBar
|
||||
:progress="'progress' in op ? (op.progress ?? 0) : 0"
|
||||
:max="1"
|
||||
:color="op.state === 'done' ? 'green' : op.state?.startsWith('fail') ? 'red' : 'blue'"
|
||||
:waiting="op.state === 'queued' || !op.progress || op.progress === 0"
|
||||
full-width
|
||||
/>
|
||||
</template>
|
||||
</Admonition>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PackageOpenIcon, XIcon } from '@modrinth/assets'
|
||||
import { formatBytes } from '@modrinth/utils'
|
||||
|
||||
import Admonition from '#ui/components/base/Admonition.vue'
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import ProgressBar from '#ui/components/base/ProgressBar.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { injectModrinthServerContext } from '#ui/providers'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
extracting: {
|
||||
id: 'files.operations.extracting',
|
||||
defaultMessage: 'Extracting {source}',
|
||||
},
|
||||
modpackFromUrl: {
|
||||
id: 'files.operations.modpack-from-url',
|
||||
defaultMessage: 'modpack from URL',
|
||||
},
|
||||
failed: {
|
||||
id: 'files.operations.failed',
|
||||
defaultMessage: 'Failed',
|
||||
},
|
||||
extracted: {
|
||||
id: 'files.operations.extracted',
|
||||
defaultMessage: '{size} extracted',
|
||||
},
|
||||
})
|
||||
|
||||
const ctx = injectModrinthServerContext()
|
||||
|
||||
const activeOperations = ctx.activeOperations
|
||||
</script>
|
||||
@@ -32,10 +32,6 @@
|
||||
>
|
||||
</FileContextMenu>
|
||||
<div v-if="!(ctx.loading.value && items.length === 0)" class="contents">
|
||||
<Admonition v-if="ctx.busyWarning?.value" type="warning" class="mb-5">
|
||||
<template #header>{{ ctx.busyWarning.value }}</template>
|
||||
{{ formatMessage(messages.busyWarning) }}
|
||||
</Admonition>
|
||||
<div class="relative flex w-full flex-col">
|
||||
<div class="relative isolate flex w-full flex-col gap-4">
|
||||
<FileNavbar
|
||||
@@ -210,7 +206,6 @@ import {
|
||||
import type { Component } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import Admonition from '#ui/components/base/Admonition.vue'
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import FloatingActionBar from '#ui/components/base/FloatingActionBar.vue'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
@@ -244,10 +239,6 @@ import type { FileContextMenuOption, FileItem } from './types'
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
busyWarning: {
|
||||
id: 'files.layout.busy-warning',
|
||||
defaultMessage: 'File operations are disabled while the operation is in progress.',
|
||||
},
|
||||
emptyFolderTitle: {
|
||||
id: 'files.layout.empty-folder-title',
|
||||
defaultMessage: 'This folder is empty',
|
||||
|
||||
Reference in New Issue
Block a user