Compare commits

...
Author SHA1 Message Date
Calum H. (IMB11) 8642134a61 fix: abort signal not processing 2026-05-16 17:15:49 +01:00
Calum H. (IMB11) 680301ed9d fix: file uploading state admonition 2026-05-16 17:03:56 +01:00
9 changed files with 90 additions and 40 deletions
@@ -229,7 +229,6 @@ async function handleDownloadFile(path: string, _fileName: string) {
const uploadState = ref<UploadState>({
isUploading: false,
currentFileName: null,
currentFileProgress: 0,
uploadedBytes: 0,
totalBytes: 0,
completedFiles: 0,
@@ -241,8 +240,7 @@ async function handleUploadFiles(files: File[]) {
uploadState.value = {
isUploading: true,
currentFileName: '',
currentFileProgress: 0,
currentFileName: files[0]?.name ?? null,
uploadedBytes: 0,
totalBytes: files.reduce((sum, f) => sum + f.size, 0),
completedFiles: 0,
@@ -258,7 +256,6 @@ async function handleUploadFiles(files: File[]) {
await writeFileBytes(targetPath, new Uint8Array(buffer))
uploadState.value.completedFiles++
uploadState.value.uploadedBytes += file.size
uploadState.value.currentFileProgress = 1
}
} catch (e) {
addNotification({
@@ -96,13 +96,17 @@ export abstract class XHRUploadClient extends AbstractModrinthClient {
return new Promise<T>((resolve, reject) => {
const xhr = new XMLHttpRequest()
const metadata = context.metadata as UploadMetadata
const fallbackTotal = this.getUploadPayloadSize(metadata)
const abortUpload = () => xhr.abort()
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const total = e.lengthComputable ? e.total : fallbackTotal
if (total > 0) {
const loaded = Math.min(e.loaded, total)
const progress: UploadProgress = {
loaded: e.loaded,
total: e.total,
progress: e.loaded / e.total,
loaded,
total,
progress: loaded / total,
}
progressCallbacks.forEach((cb) => cb(progress))
}
@@ -122,6 +126,12 @@ export abstract class XHRUploadClient extends AbstractModrinthClient {
xhr.addEventListener('error', () => reject(new ModrinthApiError('Upload failed')))
xhr.addEventListener('abort', () => reject(new ModrinthApiError('Upload cancelled')))
abortController.signal.addEventListener('abort', abortUpload, { once: true })
if (abortController.signal.aborted) {
reject(new ModrinthApiError('Upload cancelled'))
return
}
// build URL with params (unlike $fetch, XHR doesn't handle params automatically)
let url = context.url
@@ -140,12 +150,23 @@ export abstract class XHRUploadClient extends AbstractModrinthClient {
}
// Send either FormData or file depending on what was provided
const data = 'formData' in metadata ? metadata.formData : metadata.file
const data = metadata.formData instanceof FormData ? metadata.formData : metadata.file
xhr.send(data)
abortController.signal.addEventListener('abort', () => xhr.abort())
})
}
private getUploadPayloadSize(metadata: UploadMetadata): number {
if (metadata.formData instanceof FormData) {
let total = 0
metadata.formData.forEach((value) => {
total += value instanceof Blob ? value.size : new Blob([value]).size
})
return total
}
return metadata.file instanceof Blob ? metadata.file.size : 0
}
protected createUploadError(xhr: XMLHttpRequest): ModrinthApiError {
let responseData: unknown
try {
-1
View File
@@ -93,7 +93,6 @@ export interface UploadHandle<T> {
export interface UploadState {
isUploading: boolean
currentFileName: string | null
currentFileProgress: number
uploadedBytes: number
totalBytes: number
completedFiles: number
@@ -1,18 +1,14 @@
<template>
<Admonition type="info" :progress="overallProgress" progress-color="blue">
<Admonition type="info" :progress="displayProgress" progress-color="blue">
<template #icon>
<UploadIcon class="h-6 w-6 flex-none text-brand-blue" />
</template>
<template #header>
{{
state.currentFileName
? `Uploading ${state.currentFileName} (${state.completedFiles}/${state.totalFiles})`
: `Uploading files (${state.completedFiles}/${state.totalFiles})`
}}
{{ headerText }}
</template>
<span class="text-secondary">
{{ formatBytes(state.uploadedBytes) }} / {{ formatBytes(state.totalBytes) }} ({{
Math.round(overallProgress * 100)
{{ formatBytes(displayUploadedBytes) }} / {{ formatBytes(state.totalBytes) }} ({{
Math.round(displayProgress * 100)
}}%)
</span>
<template v-if="cancelUpload" #top-right-actions>
@@ -39,9 +35,32 @@ const ctx = injectModrinthServerContext()
const state = computed(() => ctx.uploadState.value)
const cancelUpload = computed(() => ctx.cancelUpload.value)
const overallProgress = computed(() => {
const headerText = computed(() => {
const s = state.value
if (!s.isUploading || s.totalFiles === 0) return 0
return Math.min((s.completedFiles + s.currentFileProgress) / s.totalFiles, 1)
if (s.currentFileName) {
return `Uploading ${s.currentFileName} (${currentFileNumber.value}/${s.totalFiles})`
}
return `Uploading files (${s.completedFiles}/${s.totalFiles})`
})
const currentFileNumber = computed(() => {
const s = state.value
if (s.totalFiles === 0) return 0
return Math.min(s.completedFiles + 1, s.totalFiles)
})
const displayUploadedBytes = computed(() => {
const s = state.value
if (s.totalBytes <= 0) return s.uploadedBytes
return Math.min(s.uploadedBytes, s.totalBytes)
})
const displayProgress = computed(() => {
const s = state.value
if (!s.isUploading) return 0
if (s.totalBytes > 0) {
return displayUploadedBytes.value / s.totalBytes
}
return 0
})
</script>
@@ -349,7 +349,6 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
const uploadState = ref<UploadState>({
isUploading: false,
currentFileName: null,
currentFileProgress: 0,
uploadedBytes: 0,
totalBytes: 0,
completedFiles: 0,
@@ -702,31 +702,53 @@ function handleUploadFiles() {
input.onchange = async () => {
if (!input.files) return
const files = Array.from(input.files)
if (files.length === 0) return
const wid = worldId.value
if (!wid) return
const totalBytes = files.reduce((sum, f) => sum + f.size, 0)
uploadState.value = {
isUploading: true,
currentFileName: null,
currentFileProgress: 0,
currentFileName: files[0]?.name ?? null,
uploadedBytes: 0,
totalBytes: files.reduce((sum, f) => sum + f.size, 0),
totalBytes,
completedFiles: 0,
totalFiles: files.length,
}
const cumulativeFileBytes = files.reduce<number[]>((out, file) => {
out.push((out[out.length - 1] ?? 0) + file.size)
return out
}, [])
function updateUploadProgress(loaded: number, total: number) {
const uploadedBytes =
total > 0 ? Math.round((Math.min(loaded, total) / total) * totalBytes) : loaded
const clampedUploadedBytes = Math.min(uploadedBytes, totalBytes)
const completedFiles = cumulativeFileBytes.filter(
(bytes) => bytes <= clampedUploadedBytes,
).length
const currentFileIndex = Math.min(completedFiles, files.length - 1)
uploadState.value.uploadedBytes = clampedUploadedBytes
uploadState.value.completedFiles = completedFiles
uploadState.value.currentFileName = files[currentFileIndex]?.name ?? null
if (clampedUploadedBytes >= totalBytes) {
cancelUpload.value = null
}
}
const handle = client.kyros.content_v1.uploadAddonFile(wid, files, {
onProgress: (p) => {
uploadState.value.currentFileProgress = p.progress
uploadState.value.uploadedBytes = p.loaded
uploadState.value.totalBytes = p.total
},
onProgress: ({ loaded, total }) => updateUploadProgress(loaded, total),
})
cancelUpload.value = () => handle.cancel()
try {
await handle.promise
cancelUpload.value = null
uploadState.value.uploadedBytes = totalBytes
uploadState.value.completedFiles = files.length
uploadState.value.currentFileName = files[files.length - 1]?.name ?? null
await contentQuery.refetch()
} catch (err) {
if (err instanceof Error && err.message === 'Upload cancelled') return
@@ -740,7 +762,6 @@ function handleUploadFiles() {
uploadState.value = {
isUploading: false,
currentFileName: null,
currentFileProgress: 0,
uploadedBytes: 0,
totalBytes: 0,
completedFiles: 0,
@@ -374,7 +374,6 @@ async function uploadFiles(files: File[]) {
uploadState.value = {
isUploading: true,
currentFileName: files[0].name,
currentFileProgress: 0,
uploadedBytes: 0,
totalBytes,
completedFiles: 0,
@@ -389,13 +388,11 @@ async function uploadFiles(files: File[]) {
const filePath = `${currentPath.value}/${file.name}`.replace('//', '/')
uploadState.value.currentFileName = file.name
uploadState.value.currentFileProgress = 0
try {
const uploader = client.kyros.files_v0.uploadFile(filePath, file, {
onProgress: ({ progress }) => {
uploadState.value.currentFileProgress = progress
uploadState.value.uploadedBytes = completedBytes + Math.round(file.size * progress)
onProgress: ({ loaded }) => {
uploadState.value.uploadedBytes = completedBytes + Math.min(loaded, file.size)
},
})
activeUploadCancel = () => uploader.cancel()
@@ -420,7 +417,6 @@ async function uploadFiles(files: File[]) {
uploadState.value = {
isUploading: false,
currentFileName: null,
currentFileProgress: 0,
uploadedBytes: 0,
totalBytes: 0,
completedFiles: 0,
@@ -46,7 +46,6 @@ const meta = {
const uploadState = ref<UploadState>({
isUploading: false,
currentFileName: null,
currentFileProgress: 0,
uploadedBytes: 0,
totalBytes: 0,
completedFiles: 0,
@@ -52,7 +52,6 @@ const meta = {
const uploadState = ref<UploadState>({
isUploading: true,
currentFileName: 'resourcepack.zip',
currentFileProgress: 0.2,
uploadedBytes: 20_000,
totalBytes: 100_000,
completedFiles: 1,