From 2d6fcd0a86a5341d3594b6bb784c59be35906984 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 29 Jun 2026 23:50:30 +0100 Subject: [PATCH] fix: better handling of larger instances for ExportModal --- .../src/components/ui/ExportModal.vue | 153 +++++++++++------- .../app-frontend/src/pages/instance/Index.vue | 2 +- .../app-lib/src/api/instance/export_mrpack.rs | 34 ++-- 3 files changed, 116 insertions(+), 73 deletions(-) diff --git a/apps/app-frontend/src/components/ui/ExportModal.vue b/apps/app-frontend/src/components/ui/ExportModal.vue index 85891bbe08..e97f833f5c 100644 --- a/apps/app-frontend/src/components/ui/ExportModal.vue +++ b/apps/app-frontend/src/components/ui/ExportModal.vue @@ -57,7 +57,7 @@ defineExpose({ show: () => { resetExportState() exportModal.value.show() - initFiles() + void initFiles().catch(handleError) }, }) @@ -69,34 +69,29 @@ const files = ref([]) const selectedFilePaths = ref([]) const fileTreeKey = ref(0) const filesLoadId = ref(0) +const instanceRoot = ref('') +const loadedDirectories = ref(new Set()) -const initFiles = async () => { +async function initFiles() { const loadId = ++filesLoadId.value - const [filePaths, instanceRoot] = await Promise.all([ + const [filePaths, root] = await Promise.all([ get_pack_export_candidates(props.instance.id), get_full_path(props.instance.id), ]) - const expandedFiles = await Promise.all( - filePaths.map((path) => expandExportCandidate(instanceRoot, path)), + if (loadId !== filesLoadId.value) return + + instanceRoot.value = root + const exportCandidates = await Promise.all( + filePaths.map((path) => buildExportCandidateItem(root, path)), ) if (loadId !== filesLoadId.value) return - files.value = expandedFiles.flat() + files.value = exportCandidates selectedFilePaths.value = files.value - .filter( - (file) => - !file.disabled && - (file.path.startsWith('mods') || - file.path.startsWith('datapacks') || - file.path.startsWith('resourcepacks') || - file.path.startsWith('shaderpacks') || - file.path.startsWith('config')), - ) + .filter((file) => !file.disabled && isDefaultSelectedExportCandidate(file.path)) .map((file) => file.path) } -await initFiles() - const exportPack = async () => { const outputPath = await save({ defaultPath: `${nameInput.value} ${versionInput.value}.mrpack`, @@ -128,58 +123,79 @@ function resetExportState() { files.value = [] selectedFilePaths.value = [] fileTreeKey.value += 1 + instanceRoot.value = '' + loadedDirectories.value = new Set() } -async function expandExportCandidate(instanceRoot, path) { +async function loadExportDirectory(path) { + if (!path || !instanceRoot.value || loadedDirectories.value.has(path)) return + + const loadId = filesLoadId.value + loadedDirectories.value.add(path) + + try { + const entries = await readDir(`${instanceRoot.value}/${path}`) + const childItems = await Promise.all( + entries.map((entry) => buildExportDirectoryChildItem(instanceRoot.value, path, entry)), + ) + if (loadId !== filesLoadId.value) return + + appendExportItems(childItems) + } catch { + loadedDirectories.value.delete(path) + } +} + +async function buildExportCandidateItem(instanceRoot, path) { try { const entries = await readDir(`${instanceRoot}/${path}`) - if (entries.length === 0) { - const metadata = await getExportCandidateMetadata(instanceRoot, path) - return [ - { - path, - type: 'directory', - disabled: true, - modified: metadata.modified, - count: 0, - }, - ] - } - - const children = await Promise.all( - entries.map(async (entry) => { - const childPath = `${path}/${entry.name}` - if (entry.isDirectory) { - return expandExportCandidate(instanceRoot, childPath) - } - - const metadata = await getExportCandidateMetadata(instanceRoot, childPath) - return [ - { - path: childPath, - type: 'file', - disabled: isExportCandidateDisabled(childPath), - size: metadata.size, - modified: metadata.modified, - }, - ] - }), - ) - return children.flat() - } catch { const metadata = await getExportCandidateMetadata(instanceRoot, path) - return [ - { - path, - type: 'file', - disabled: isExportCandidateDisabled(path), - size: metadata.size, - modified: metadata.modified, - }, - ] + return { + path, + type: 'directory', + disabled: isExportCandidateDisabled(path), + modified: metadata.modified, + count: entries.length, + } + } catch { + return buildExportFileItem(instanceRoot, path) } } +async function buildExportDirectoryChildItem(instanceRoot, parentPath, entry) { + const path = `${parentPath}/${entry.name}` + if (entry.isDirectory) { + const metadata = await getExportCandidateMetadata(instanceRoot, path) + return { + path, + type: 'directory', + disabled: isExportCandidateDisabled(path), + modified: metadata.modified, + } + } + + return buildExportFileItem(instanceRoot, path) +} + +async function buildExportFileItem(instanceRoot, path) { + const metadata = await getExportCandidateMetadata(instanceRoot, path) + return { + path, + type: 'file', + disabled: isExportCandidateDisabled(path), + size: metadata.size, + modified: metadata.modified, + } +} + +function appendExportItems(items) { + const nextFiles = new Map(files.value.map((file) => [normalizeExportPath(file.path), file])) + for (const item of items) { + nextFiles.set(normalizeExportPath(item.path), item) + } + files.value = [...nextFiles.values()] +} + async function getExportCandidateMetadata(instanceRoot, path) { try { const metadata = await stat(`${instanceRoot}/${path}`) @@ -192,6 +208,20 @@ async function getExportCandidateMetadata(instanceRoot, path) { } } +function normalizeExportPath(path) { + return path.replaceAll('\\', '/').split('/').filter(Boolean).join('/') +} + +function isDefaultSelectedExportCandidate(path) { + return ( + path.startsWith('mods') || + path.startsWith('datapacks') || + path.startsWith('resourcepacks') || + path.startsWith('shaderpacks') || + path.startsWith('config') + ) +} + function isExportCandidateDisabled(path) { return ( path === 'profile.json' || @@ -251,6 +281,7 @@ function isExportCandidateDisabled(path) { v-model="selectedFilePaths" class="min-w-0" :items="files" + @navigate="loadExportDirectory" />