mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
fix: export modal path handling (#6949)
* fix: export modal path handling * fix: prepr * fix: NEVER_EXPORTABLE_PATH_SUFFIXES list
This commit is contained in:
@@ -11,15 +11,10 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { save } from '@tauri-apps/plugin-dialog'
|
||||
import { readDir, stat } from '@tauri-apps/plugin-fs'
|
||||
import { ref } from 'vue'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
|
||||
import { PackageIcon } from '@/assets/icons'
|
||||
import {
|
||||
export_instance_mrpack,
|
||||
get_full_path,
|
||||
get_pack_export_candidates,
|
||||
} from '@/helpers/instance'
|
||||
import { export_instance_mrpack, get_pack_export_candidates } from '@/helpers/instance'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -65,30 +60,24 @@ const exportModal = ref(null)
|
||||
const nameInput = ref(props.instance.name)
|
||||
const exportDescription = ref('')
|
||||
const versionInput = ref('1.0.0')
|
||||
const files = ref([])
|
||||
const selectedFilePaths = ref([])
|
||||
const files = shallowRef([])
|
||||
const includedFilePaths = ref([])
|
||||
const excludedFilePaths = ref([])
|
||||
const fileTreeKey = ref(0)
|
||||
const filesLoadId = ref(0)
|
||||
const instanceRoot = ref('')
|
||||
const loadedDirectories = ref(new Set())
|
||||
const directoryEntries = new Map()
|
||||
const currentDirectory = ref('')
|
||||
|
||||
async function initFiles() {
|
||||
const loadId = ++filesLoadId.value
|
||||
const [filePaths, root] = await Promise.all([
|
||||
get_pack_export_candidates(props.instance.id),
|
||||
get_full_path(props.instance.id),
|
||||
])
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
instanceRoot.value = root
|
||||
const exportCandidates = await Promise.all(
|
||||
filePaths.map((path) => buildExportCandidateItem(root, path)),
|
||||
)
|
||||
const exportCandidates = await get_pack_export_candidates(props.instance.id)
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
files.value = exportCandidates
|
||||
selectedFilePaths.value = files.value
|
||||
.filter((file) => !file.disabled && isDefaultSelectedExportCandidate(file.path))
|
||||
directoryEntries.set('', exportCandidates)
|
||||
currentDirectory.value = ''
|
||||
includedFilePaths.value = files.value
|
||||
.filter((file) => !file.disabled && file.defaultSelected)
|
||||
.map((file) => file.path)
|
||||
}
|
||||
|
||||
@@ -107,7 +96,8 @@ const exportPack = async () => {
|
||||
export_instance_mrpack(
|
||||
props.instance.id,
|
||||
outputPath,
|
||||
selectedFilePaths.value,
|
||||
includedFilePaths.value,
|
||||
excludedFilePaths.value,
|
||||
versionInput.value,
|
||||
exportDescription.value,
|
||||
nameInput.value,
|
||||
@@ -121,115 +111,45 @@ function resetExportState() {
|
||||
exportDescription.value = ''
|
||||
versionInput.value = '1.0.0'
|
||||
files.value = []
|
||||
selectedFilePaths.value = []
|
||||
includedFilePaths.value = []
|
||||
excludedFilePaths.value = []
|
||||
fileTreeKey.value += 1
|
||||
instanceRoot.value = ''
|
||||
loadedDirectories.value = new Set()
|
||||
directoryEntries.clear()
|
||||
currentDirectory.value = ''
|
||||
}
|
||||
|
||||
async function loadExportDirectory(path) {
|
||||
if (!path || !instanceRoot.value || loadedDirectories.value.has(path)) return
|
||||
const normalizedPath = normalizeExportPath(path)
|
||||
currentDirectory.value = normalizedPath
|
||||
|
||||
const cachedEntries = directoryEntries.get(normalizedPath)
|
||||
if (cachedEntries) {
|
||||
files.value = cachedEntries
|
||||
return
|
||||
}
|
||||
|
||||
const loadId = filesLoadId.value
|
||||
loadedDirectories.value.add(path)
|
||||
files.value = []
|
||||
|
||||
try {
|
||||
const entries = await readDir(`${instanceRoot.value}/${path}`)
|
||||
const childItems = await Promise.all(
|
||||
entries.map((entry) => buildExportDirectoryChildItem(instanceRoot.value, path, entry)),
|
||||
const childItems = await get_pack_export_candidates(
|
||||
props.instance.id,
|
||||
normalizedPath || undefined,
|
||||
)
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
appendExportItems(childItems)
|
||||
} catch {
|
||||
loadedDirectories.value.delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
async function buildExportCandidateItem(instanceRoot, path) {
|
||||
try {
|
||||
const entries = await readDir(`${instanceRoot}/${path}`)
|
||||
const metadata = await getExportCandidateMetadata(instanceRoot, path)
|
||||
return {
|
||||
path,
|
||||
type: 'directory',
|
||||
disabled: isExportCandidateDisabled(path),
|
||||
modified: metadata.modified,
|
||||
count: entries.length,
|
||||
directoryEntries.set(normalizedPath, childItems)
|
||||
if (currentDirectory.value === normalizedPath) {
|
||||
files.value = childItems
|
||||
}
|
||||
} 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}`)
|
||||
return {
|
||||
size: metadata.size,
|
||||
modified: metadata.mtime ? Math.floor(metadata.mtime.getTime() / 1000) : undefined,
|
||||
}
|
||||
} catch {
|
||||
return {}
|
||||
if (currentDirectory.value === normalizedPath) files.value = []
|
||||
}
|
||||
}
|
||||
|
||||
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' ||
|
||||
path.startsWith('modrinth_logs') ||
|
||||
path.startsWith('.fabric') ||
|
||||
path.startsWith('__MACOSX')
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -278,9 +198,11 @@ function isExportCandidateDisabled(path) {
|
||||
</div>
|
||||
<FileTreeSelect
|
||||
:key="fileTreeKey"
|
||||
v-model="selectedFilePaths"
|
||||
v-model="includedFilePaths"
|
||||
v-model:excluded-paths="excludedFilePaths"
|
||||
class="min-w-0"
|
||||
:items="files"
|
||||
lazy
|
||||
@navigate="loadExportDirectory"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+31
-3
@@ -28,7 +28,8 @@
|
||||
<div class="flex min-w-0 flex-col gap-3 pt-4">
|
||||
<div ref="configFileTreeContainer" class="max-h-[292px] overflow-y-auto rounded-[20px]">
|
||||
<FileTreeSelect
|
||||
v-model="selectedConfigPaths"
|
||||
v-model="includedConfigPaths"
|
||||
v-model:excluded-paths="excludedConfigPaths"
|
||||
:items="configFileItems"
|
||||
:show-size="false"
|
||||
:show-modified="false"
|
||||
@@ -79,11 +80,20 @@ const publishReviewModal = ref<InstanceType<typeof ContentDiffModal>>()
|
||||
const configFileTreeContainer = ref<HTMLElement>()
|
||||
const publishDiffs = ref<ContentDiffItem[]>([])
|
||||
const configFilePaths = ref<string[]>([])
|
||||
const selectedConfigPaths = ref<string[]>([])
|
||||
const includedConfigPaths = ref<string[]>([])
|
||||
const excludedConfigPaths = ref<string[]>([])
|
||||
const state = ref<SharedInstancePublishState>('idle')
|
||||
const configFileItems = computed<FileTreeSelectItem[]>(() =>
|
||||
configFilePaths.value.map((path) => ({ path, type: 'file' })),
|
||||
)
|
||||
const selectedConfigPaths = computed(() => {
|
||||
const includedPaths = new Set(includedConfigPaths.value)
|
||||
const excludedPaths = new Set(excludedConfigPaths.value)
|
||||
|
||||
return configFilePaths.value.filter((path) =>
|
||||
isConfigPathSelected(path, includedPaths, excludedPaths),
|
||||
)
|
||||
})
|
||||
|
||||
async function show(e?: MouseEvent) {
|
||||
if (state.value !== 'idle') return
|
||||
@@ -108,7 +118,8 @@ async function show(e?: MouseEvent) {
|
||||
disabled: diff.disabled,
|
||||
}))
|
||||
configFilePaths.value = preview.configFiles
|
||||
selectedConfigPaths.value = []
|
||||
includedConfigPaths.value = []
|
||||
excludedConfigPaths.value = []
|
||||
if (!publishReviewModal.value) return
|
||||
|
||||
publishReviewModal.value.show(e)
|
||||
@@ -134,6 +145,23 @@ async function publishChanges() {
|
||||
}
|
||||
}
|
||||
|
||||
function isConfigPathSelected(
|
||||
path: string,
|
||||
includedPaths: Set<string>,
|
||||
excludedPaths: Set<string>,
|
||||
) {
|
||||
let selected = false
|
||||
let prefix = ''
|
||||
|
||||
for (const segment of path.split('/').filter(Boolean)) {
|
||||
prefix = prefix ? `${prefix}/${segment}` : segment
|
||||
if (includedPaths.has(prefix)) selected = true
|
||||
if (excludedPaths.has(prefix)) selected = false
|
||||
}
|
||||
|
||||
return selected
|
||||
}
|
||||
|
||||
function scrollConfigFileTreeToTop() {
|
||||
if (configFileTreeContainer.value) {
|
||||
configFileTreeContainer.value.scrollTop = 0
|
||||
|
||||
@@ -282,12 +282,13 @@ export async function update_repair_modrinth(instanceId: string): Promise<Instal
|
||||
}
|
||||
|
||||
// Export an instance to .mrpack
|
||||
// included_overrides is an array of paths to override folders to include (ie: 'mods', 'resource_packs')
|
||||
// included_overrides and excluded_overrides are inherited path rules for files in the export.
|
||||
// Version id is optional (ie: 1.1.5)
|
||||
export async function export_instance_mrpack(
|
||||
instanceId: string,
|
||||
exportLocation: string,
|
||||
includedOverrides: string[],
|
||||
excludedOverrides: string[],
|
||||
versionId?: string,
|
||||
description?: string,
|
||||
name?: string,
|
||||
@@ -296,22 +297,33 @@ export async function export_instance_mrpack(
|
||||
instanceId,
|
||||
exportLocation,
|
||||
includedOverrides,
|
||||
excludedOverrides,
|
||||
versionId,
|
||||
description,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
// Given a folder path, populate an array of all the subfolders
|
||||
// Intended to be used for finding potential override folders
|
||||
// profile
|
||||
// -- mods
|
||||
// -- resourcepacks
|
||||
// -- file1
|
||||
// => [mods, resourcepacks]
|
||||
// allows selection for 'included_overrides' in export_instance_mrpack
|
||||
export async function get_pack_export_candidates(instanceId: string): Promise<string[]> {
|
||||
return await invoke('plugin:instance|instance_get_pack_export_candidates', { instanceId })
|
||||
export type PackExportCandidate = {
|
||||
path: string
|
||||
type: 'directory' | 'file'
|
||||
size?: number
|
||||
modified?: number
|
||||
count?: number
|
||||
disabled: boolean
|
||||
defaultSelected: boolean
|
||||
}
|
||||
|
||||
// Given a folder path, populate an array of exportable direct children.
|
||||
// Allows selection for 'included_overrides' in export_instance_mrpack.
|
||||
export async function get_pack_export_candidates(
|
||||
instanceId: string,
|
||||
parent?: string,
|
||||
): Promise<PackExportCandidate[]> {
|
||||
return await invoke('plugin:instance|instance_get_pack_export_candidates', {
|
||||
instanceId,
|
||||
parent: parent ?? null,
|
||||
})
|
||||
}
|
||||
|
||||
// Run Minecraft using an instance
|
||||
|
||||
@@ -729,6 +729,7 @@ pub async fn instance_export_mrpack(
|
||||
instance_id: &str,
|
||||
export_location: PathBuf,
|
||||
included_overrides: Vec<String>,
|
||||
excluded_overrides: Vec<String>,
|
||||
version_id: Option<String>,
|
||||
description: Option<String>,
|
||||
name: Option<String>,
|
||||
@@ -737,6 +738,7 @@ pub async fn instance_export_mrpack(
|
||||
instance_id,
|
||||
export_location,
|
||||
included_overrides,
|
||||
excluded_overrides,
|
||||
version_id,
|
||||
description,
|
||||
name,
|
||||
@@ -748,8 +750,13 @@ pub async fn instance_export_mrpack(
|
||||
#[tauri::command]
|
||||
pub async fn instance_get_pack_export_candidates(
|
||||
instance_id: &str,
|
||||
) -> Result<Vec<SafeRelativeUtf8UnixPathBuf>> {
|
||||
Ok(theseus::instance::get_pack_export_candidates(instance_id).await?)
|
||||
parent: Option<SafeRelativeUtf8UnixPathBuf>,
|
||||
) -> Result<Vec<theseus::instance::PackExportCandidate>> {
|
||||
Ok(theseus::instance::get_pack_export_candidates_for_parent(
|
||||
instance_id,
|
||||
parent,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
Reference in New Issue
Block a user