mirror of
https://github.com/modrinth/code.git
synced 2026-09-04 05:48:57 +00:00
refactor: implement download modal provider
This commit is contained in:
@@ -23,262 +23,35 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Labrinth } from '@modrinth/api-client'
|
import { InfoIcon } from '@modrinth/assets'
|
||||||
import { FileIcon, InfoIcon } from '@modrinth/assets'
|
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||||
import {
|
import { computed } from 'vue'
|
||||||
type CdnDownloadReason,
|
|
||||||
defineMessages,
|
|
||||||
fileTypeMessages,
|
|
||||||
injectModrinthClient,
|
|
||||||
useVIntl,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
import type { DisplayProjectType } from '@modrinth/utils'
|
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
|
||||||
import { type Component, computed, watch } from 'vue'
|
|
||||||
|
|
||||||
import DownloadDependency from './DownloadDependency.vue'
|
import DownloadDependency from './DownloadDependency.vue'
|
||||||
|
import { injectDownloadModalProvider } from './download-modal-provider'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'DownloadDependencies',
|
name: 'DownloadDependencies',
|
||||||
})
|
})
|
||||||
|
|
||||||
type DownloadModalProject = Omit<Labrinth.Projects.v2.Project, 'project_type'> & {
|
|
||||||
project_type: DisplayProjectType
|
|
||||||
actualProjectType: Labrinth.Projects.v2.ProjectType
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResolvedContent = Labrinth.Content.v3.ResolvedContent | Labrinth.Content.v3.SkippedContent
|
|
||||||
|
|
||||||
interface DownloadDependencyRow {
|
|
||||||
key: string
|
|
||||||
name: string
|
|
||||||
icon?: string
|
|
||||||
fallbackIcon?: Component
|
|
||||||
projectHref?: string
|
|
||||||
downloadHref?: string
|
|
||||||
filename?: string
|
|
||||||
fileSize?: number
|
|
||||||
typeLabel: string
|
|
||||||
unavailableTooltip: string
|
|
||||||
dependencies: DownloadDependencyRow[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DownloadableDependencyFile {
|
|
||||||
href: string
|
|
||||||
filename: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
dependencies?: DownloadDependencyRow[] | null
|
|
||||||
project?: DownloadModalProject | null
|
|
||||||
selectedVersion?: Labrinth.Versions.v3.Version | null
|
|
||||||
currentGameVersion?: string | null
|
|
||||||
currentPlatform?: string | null
|
|
||||||
downloadReason?: CdnDownloadReason
|
|
||||||
additionalFiles?: Labrinth.Versions.v3.VersionFile[]
|
|
||||||
showTitle?: boolean
|
showTitle?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
dependencies: null,
|
|
||||||
project: null,
|
|
||||||
selectedVersion: null,
|
|
||||||
currentGameVersion: null,
|
|
||||||
currentPlatform: null,
|
|
||||||
downloadReason: 'standalone',
|
|
||||||
additionalFiles: () => [],
|
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
download: []
|
download: []
|
||||||
'update:downloadable-files': [files: DownloadableDependencyFile[]]
|
|
||||||
'update:downloadable-files-loaded': [loaded: boolean]
|
|
||||||
}>()
|
}>()
|
||||||
const client = injectModrinthClient()
|
|
||||||
const { createProjectDownloadUrl } = useCdnDownloadContext()
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
const {
|
||||||
const shouldResolveDependencies = computed(
|
visibleDependencyRows,
|
||||||
() => !props.dependencies && !!props.project && !!props.selectedVersion,
|
duplicateDependencyRowsHidden,
|
||||||
)
|
downloadRows,
|
||||||
|
} = injectDownloadModalProvider()
|
||||||
const dependencyResolutionPreferences = computed<Labrinth.Content.v3.ResolutionPreferences>(() => ({
|
|
||||||
game_versions: props.selectedVersion?.game_versions || [],
|
|
||||||
loaders: props.currentPlatform ? [props.currentPlatform] : props.selectedVersion?.loaders || [],
|
|
||||||
}))
|
|
||||||
|
|
||||||
const { data: dependencyResolution, isFetching: dependencyResolutionFetching } = useQuery({
|
|
||||||
queryKey: computed(() => [
|
|
||||||
'project-download-modal',
|
|
||||||
'content-resolve',
|
|
||||||
props.project?.id,
|
|
||||||
props.selectedVersion?.id,
|
|
||||||
props.project?.project_type,
|
|
||||||
dependencyResolutionPreferences.value,
|
|
||||||
]),
|
|
||||||
queryFn: () =>
|
|
||||||
client.labrinth.content_v3.resolve({
|
|
||||||
project_id: props.project!.id,
|
|
||||||
version_id: props.selectedVersion!.id,
|
|
||||||
content_type: resolveContentType(props.project!.project_type),
|
|
||||||
selected: dependencyResolutionPreferences.value,
|
|
||||||
target: dependencyResolutionPreferences.value,
|
|
||||||
}),
|
|
||||||
enabled: shouldResolveDependencies,
|
|
||||||
})
|
|
||||||
|
|
||||||
const visibleResolvedDependencies = computed<ResolvedContent[]>(() => {
|
|
||||||
return [
|
|
||||||
...(dependencyResolution.value?.dependencies || []),
|
|
||||||
...(dependencyResolution.value?.skipped || []),
|
|
||||||
].filter(shouldShowDependency)
|
|
||||||
})
|
|
||||||
|
|
||||||
const dependencyVersionIds = computed<string[]>(() => {
|
|
||||||
return [
|
|
||||||
...new Set(
|
|
||||||
visibleResolvedDependencies.value
|
|
||||||
.filter((dependency) => !('reason' in dependency))
|
|
||||||
.map((dependency) => dependency.version_id)
|
|
||||||
.filter((versionId): versionId is string => !!versionId),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data: dependencyVersions, isFetching: dependencyVersionsFetching } = useQuery({
|
|
||||||
queryKey: computed(() => [
|
|
||||||
'project-download-modal',
|
|
||||||
'resolved-versions',
|
|
||||||
dependencyVersionIds.value,
|
|
||||||
]),
|
|
||||||
queryFn: () => client.labrinth.versions_v3.getVersions(dependencyVersionIds.value),
|
|
||||||
enabled: computed(() => shouldResolveDependencies.value && dependencyVersionIds.value.length > 0),
|
|
||||||
})
|
|
||||||
|
|
||||||
const dependencyVersionById = computed(() => {
|
|
||||||
const map = new Map<string, Labrinth.Versions.v3.Version>()
|
|
||||||
for (const version of dependencyVersions.value || []) {
|
|
||||||
if (!version) continue
|
|
||||||
map.set(version.id, version)
|
|
||||||
}
|
|
||||||
return map
|
|
||||||
})
|
|
||||||
|
|
||||||
const dependencyProjectIds = computed<string[]>(() => {
|
|
||||||
return [
|
|
||||||
...new Set(
|
|
||||||
visibleResolvedDependencies.value
|
|
||||||
.map((dependency) => dependency.project_id)
|
|
||||||
.filter((projectId): projectId is string => !!projectId),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data: dependencyProjects, isFetching: dependencyProjectsFetching } = useQuery({
|
|
||||||
queryKey: computed(() => [
|
|
||||||
'project-download-modal',
|
|
||||||
'resolved-projects',
|
|
||||||
dependencyProjectIds.value,
|
|
||||||
]),
|
|
||||||
queryFn: () => client.labrinth.projects_v2.getMultiple(dependencyProjectIds.value),
|
|
||||||
enabled: computed(() => shouldResolveDependencies.value && dependencyProjectIds.value.length > 0),
|
|
||||||
})
|
|
||||||
|
|
||||||
const dependencyProjectById = computed(() => {
|
|
||||||
const map = new Map<string, Labrinth.Projects.v2.Project>()
|
|
||||||
for (const project of dependencyProjects.value || []) {
|
|
||||||
map.set(project.id, project)
|
|
||||||
}
|
|
||||||
return map
|
|
||||||
})
|
|
||||||
|
|
||||||
const dependenciesByParentVersionId = computed(() => {
|
|
||||||
const map = new Map<string, ResolvedContent[]>()
|
|
||||||
|
|
||||||
for (const dependency of visibleResolvedDependencies.value) {
|
|
||||||
if (!dependency.dependent_on_version_id) continue
|
|
||||||
|
|
||||||
const dependencies = map.get(dependency.dependent_on_version_id) || []
|
|
||||||
dependencies.push(dependency)
|
|
||||||
map.set(dependency.dependent_on_version_id, dependencies)
|
|
||||||
}
|
|
||||||
|
|
||||||
return map
|
|
||||||
})
|
|
||||||
|
|
||||||
const dependenciesLoaded = computed(() => {
|
|
||||||
if (!shouldResolveDependencies.value) return false
|
|
||||||
if (dependencyResolutionFetching.value) return false
|
|
||||||
if (!dependencyResolution.value) return false
|
|
||||||
if (
|
|
||||||
dependencyResolution.value.primary.version_id &&
|
|
||||||
dependencyResolution.value.primary.version_id !== props.selectedVersion?.id
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
dependencyVersionsFetching.value ||
|
|
||||||
!dependencyVersionIds.value.every((versionId) => dependencyVersionById.value.has(versionId))
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
dependencyProjectsFetching.value ||
|
|
||||||
!dependencyProjectIds.value.every((projectId) => dependencyProjectById.value.has(projectId))
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
const resolvedDependencyRows = computed<DownloadDependencyRow[]>(() => {
|
|
||||||
if (!dependenciesLoaded.value) return []
|
|
||||||
|
|
||||||
const primaryVersionId =
|
|
||||||
dependencyResolution.value?.primary.version_id || props.selectedVersion?.id
|
|
||||||
if (!primaryVersionId) return []
|
|
||||||
|
|
||||||
const dependencies = dependenciesByParentVersionId.value.get(primaryVersionId) || []
|
|
||||||
|
|
||||||
return dependencies.flatMap((dependency) => {
|
|
||||||
const row = createDependencyRow(dependency)
|
|
||||||
return row ? [row] : []
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const dependencyRows = computed<DownloadDependencyRow[]>(
|
|
||||||
() => props.dependencies || resolvedDependencyRows.value,
|
|
||||||
)
|
|
||||||
|
|
||||||
const visibleDependencyRows = computed<DownloadDependencyRow[]>(() =>
|
|
||||||
dedupeDependencyRows(dependencyRows.value),
|
|
||||||
)
|
|
||||||
|
|
||||||
const duplicateDependencyRowsHidden = computed(() =>
|
|
||||||
hasDuplicateDependencyRows(dependencyRows.value),
|
|
||||||
)
|
|
||||||
|
|
||||||
const additionalFileRows = computed<DownloadDependencyRow[]>(() =>
|
|
||||||
props.additionalFiles.map((file) => ({
|
|
||||||
key: `additional-file-${additionalFileKey(file)}`,
|
|
||||||
name: file.filename,
|
|
||||||
fallbackIcon: FileIcon,
|
|
||||||
downloadHref: getDownloadUrl(file.url),
|
|
||||||
filename: file.filename,
|
|
||||||
fileSize: file.size,
|
|
||||||
typeLabel: fileTypeLabel(file.file_type),
|
|
||||||
unavailableTooltip: formatMessage(messages.unavailableFile),
|
|
||||||
dependencies: [],
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
|
|
||||||
const downloadRows = computed<DownloadDependencyRow[]>(() => [
|
|
||||||
...visibleDependencyRows.value,
|
|
||||||
...additionalFileRows.value,
|
|
||||||
])
|
|
||||||
|
|
||||||
const sectionTitle = computed(() =>
|
const sectionTitle = computed(() =>
|
||||||
formatMessage(
|
formatMessage(
|
||||||
@@ -288,181 +61,6 @@ const sectionTitle = computed(() =>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const downloadableDependencyFiles = computed<DownloadableDependencyFile[]>(() =>
|
|
||||||
collectDownloadableDependencyFiles(visibleDependencyRows.value),
|
|
||||||
)
|
|
||||||
|
|
||||||
const downloadableDependencyFilesLoaded = computed(() => {
|
|
||||||
if (props.dependencies) return true
|
|
||||||
if (!shouldResolveDependencies.value) return false
|
|
||||||
return dependenciesLoaded.value
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
downloadableDependencyFiles,
|
|
||||||
(files) => {
|
|
||||||
emit('update:downloadable-files', files)
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
downloadableDependencyFilesLoaded,
|
|
||||||
(loaded) => {
|
|
||||||
emit('update:downloadable-files-loaded', loaded)
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
function primaryFileForVersion(version?: Labrinth.Versions.v3.Version) {
|
|
||||||
return version?.files?.find((file) => file.primary) || version?.files?.[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldShowDependency(dependency: ResolvedContent) {
|
|
||||||
return !(
|
|
||||||
'reason' in dependency && ['duplicate_project', 'quilt_fabric_api'].includes(dependency.reason)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDependencyRow(dependency: ResolvedContent): DownloadDependencyRow | null {
|
|
||||||
const versionId = dependency.version_id ?? undefined
|
|
||||||
const version = versionId ? dependencyVersionById.value.get(versionId) : undefined
|
|
||||||
const project = dependencyProjectById.value.get(dependency.project_id)
|
|
||||||
if (!project) return null
|
|
||||||
|
|
||||||
const primaryFile = primaryFileForVersion(version)
|
|
||||||
const unavailableTooltip =
|
|
||||||
'reason' in dependency && dependency.reason
|
|
||||||
? skippedReasonLabel(dependency.reason)
|
|
||||||
: formatMessage(messages.unavailableDependency)
|
|
||||||
const name = project.title
|
|
||||||
|
|
||||||
return {
|
|
||||||
key: `${dependency.project_id}-${versionId ?? 'unresolved'}-${
|
|
||||||
'reason' in dependency ? dependency.reason : 'resolved'
|
|
||||||
}`,
|
|
||||||
name,
|
|
||||||
icon: project.icon_url ?? undefined,
|
|
||||||
projectHref: `/${project.project_type}/${project.slug || project.id}`,
|
|
||||||
downloadHref:
|
|
||||||
'reason' in dependency || !primaryFile ? undefined : getDownloadUrl(primaryFile.url),
|
|
||||||
filename: primaryFile?.filename,
|
|
||||||
fileSize: primaryFile?.size,
|
|
||||||
typeLabel: 'Required',
|
|
||||||
unavailableTooltip,
|
|
||||||
dependencies: (versionId && dependenciesByParentVersionId.value.get(versionId)
|
|
||||||
? dependenciesByParentVersionId.value.get(versionId)!
|
|
||||||
: []
|
|
||||||
).flatMap((subDependency) => {
|
|
||||||
const row = createDependencyRow(subDependency)
|
|
||||||
return row ? [row] : []
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function skippedReasonLabel(reason: Labrinth.Content.v3.SkippedContent['reason']) {
|
|
||||||
return (
|
|
||||||
{
|
|
||||||
already_installed: formatMessage(messages.alreadyInstalledDependency),
|
|
||||||
duplicate_project: formatMessage(messages.duplicateDependency),
|
|
||||||
conflicting_dependency: formatMessage(messages.conflictingDependency),
|
|
||||||
no_compatible_version: formatMessage(messages.noCompatibleDependency),
|
|
||||||
missing_version: formatMessage(messages.missingDependencyVersion),
|
|
||||||
quilt_fabric_api: formatMessage(messages.quiltFabricApiDependency),
|
|
||||||
}[reason] || formatMessage(messages.unavailableDependency)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveContentType(projectType: DisplayProjectType): Labrinth.Content.v3.ContentType {
|
|
||||||
return ['mod', 'plugin', 'datapack', 'resourcepack', 'shader', 'modpack'].includes(projectType)
|
|
||||||
? (projectType as Labrinth.Content.v3.ContentType)
|
|
||||||
: 'mod'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDownloadUrl(url: string) {
|
|
||||||
return createProjectDownloadUrl(url, {
|
|
||||||
reason: props.downloadReason,
|
|
||||||
gameVersion: props.currentGameVersion ?? undefined,
|
|
||||||
loader: props.currentPlatform ?? undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileTypeLabel(type?: Labrinth.Versions.v3.FileType | null) {
|
|
||||||
return formatMessage(fileTypeMessages[type ?? 'unknown'] ?? fileTypeMessages.unknown)
|
|
||||||
}
|
|
||||||
|
|
||||||
function additionalFileKey(file: Labrinth.Versions.v3.VersionFile) {
|
|
||||||
return file.hashes?.sha1 ?? file.filename
|
|
||||||
}
|
|
||||||
|
|
||||||
function dedupeDependencyRows(
|
|
||||||
rows: DownloadDependencyRow[],
|
|
||||||
seenDependencies = new Set<string>(),
|
|
||||||
): DownloadDependencyRow[] {
|
|
||||||
return rows.flatMap((row) => {
|
|
||||||
const identity = dependencyRowIdentity(row)
|
|
||||||
if (seenDependencies.has(identity)) return []
|
|
||||||
|
|
||||||
seenDependencies.add(identity)
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
...row,
|
|
||||||
dependencies: dedupeDependencyRows(row.dependencies, seenDependencies),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function dependencyRowIdentity(row: DownloadDependencyRow) {
|
|
||||||
return row.projectHref ?? row.downloadHref ?? row.key
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasDuplicateDependencyRows(
|
|
||||||
rows: DownloadDependencyRow[],
|
|
||||||
seenDependencies = new Set<string>(),
|
|
||||||
): boolean {
|
|
||||||
for (const row of rows) {
|
|
||||||
const rowId = dependencyRowIdentity(row)
|
|
||||||
if (seenDependencies.has(rowId)) return true
|
|
||||||
seenDependencies.add(rowId)
|
|
||||||
if (hasDuplicateDependencyRows(row.dependencies, seenDependencies)) return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectDownloadableDependencyFiles(
|
|
||||||
rows: DownloadDependencyRow[],
|
|
||||||
seenHrefs = new Set<string>(),
|
|
||||||
): DownloadableDependencyFile[] {
|
|
||||||
const files: DownloadableDependencyFile[] = []
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
if (row.downloadHref && !seenHrefs.has(row.downloadHref)) {
|
|
||||||
seenHrefs.add(row.downloadHref)
|
|
||||||
files.push({
|
|
||||||
href: row.downloadHref,
|
|
||||||
filename: row.filename || filenameFromUrl(row.downloadHref),
|
|
||||||
name: row.name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
files.push(...collectDownloadableDependencyFiles(row.dependencies, seenHrefs))
|
|
||||||
}
|
|
||||||
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
function filenameFromUrl(url: string) {
|
|
||||||
try {
|
|
||||||
const filename = new URL(url).pathname.split('/').pop()
|
|
||||||
return filename ? decodeURIComponent(filename) : 'dependency.jar'
|
|
||||||
} catch {
|
|
||||||
return 'dependency.jar'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
dependenciesTitle: {
|
dependenciesTitle: {
|
||||||
id: 'project.download.dependencies-title',
|
id: 'project.download.dependencies-title',
|
||||||
@@ -476,37 +74,5 @@ const messages = defineMessages({
|
|||||||
id: 'project.download.additional-files-title',
|
id: 'project.download.additional-files-title',
|
||||||
defaultMessage: 'Additional files',
|
defaultMessage: 'Additional files',
|
||||||
},
|
},
|
||||||
alreadyInstalledDependency: {
|
|
||||||
id: 'project.download.dependency-already-installed',
|
|
||||||
defaultMessage: 'This dependency is already installed',
|
|
||||||
},
|
|
||||||
conflictingDependency: {
|
|
||||||
id: 'project.download.dependency-conflicting',
|
|
||||||
defaultMessage: 'This dependency conflicts with another dependency',
|
|
||||||
},
|
|
||||||
duplicateDependency: {
|
|
||||||
id: 'project.download.dependency-duplicate',
|
|
||||||
defaultMessage: 'This dependency is already included',
|
|
||||||
},
|
|
||||||
missingDependencyVersion: {
|
|
||||||
id: 'project.download.dependency-missing-version',
|
|
||||||
defaultMessage: 'This dependency version is unavailable',
|
|
||||||
},
|
|
||||||
noCompatibleDependency: {
|
|
||||||
id: 'project.download.dependency-no-compatible-version',
|
|
||||||
defaultMessage: 'No compatible version is available for this dependency',
|
|
||||||
},
|
|
||||||
quiltFabricApiDependency: {
|
|
||||||
id: 'project.download.dependency-quilt-fabric-api',
|
|
||||||
defaultMessage: 'Fabric API is skipped for Quilt',
|
|
||||||
},
|
|
||||||
unavailableDependency: {
|
|
||||||
id: 'project.download.dependency-unavailable',
|
|
||||||
defaultMessage: 'This dependency cannot be downloaded',
|
|
||||||
},
|
|
||||||
unavailableFile: {
|
|
||||||
id: 'project.download.file-unavailable',
|
|
||||||
defaultMessage: 'This file cannot be downloaded',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,571 @@
|
|||||||
|
import type { AbstractModrinthClient, Labrinth } from '@modrinth/api-client'
|
||||||
|
import { FileIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
type CdnDownloadReason,
|
||||||
|
createContext,
|
||||||
|
defineMessages,
|
||||||
|
fileTypeMessages,
|
||||||
|
injectModrinthClient,
|
||||||
|
useVIntl,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import type { DisplayProjectType } from '@modrinth/utils'
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
|
import { type Component, computed, type ComputedRef } from 'vue'
|
||||||
|
|
||||||
|
import { STALE_TIME } from '~/composables/queries/project'
|
||||||
|
|
||||||
|
type DownloadModalProject = Omit<Labrinth.Projects.v2.Project, 'project_type'> & {
|
||||||
|
project_type: DisplayProjectType
|
||||||
|
actualProjectType: Labrinth.Projects.v2.ProjectType
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResolvedContent = Labrinth.Content.v3.ResolvedContent | Labrinth.Content.v3.SkippedContent
|
||||||
|
|
||||||
|
export interface DownloadDependencyRow {
|
||||||
|
key: string
|
||||||
|
name: string
|
||||||
|
icon?: string
|
||||||
|
fallbackIcon?: Component
|
||||||
|
projectHref?: string
|
||||||
|
downloadHref?: string
|
||||||
|
filename?: string
|
||||||
|
fileSize?: number
|
||||||
|
typeLabel: string
|
||||||
|
unavailableTooltip: string
|
||||||
|
dependencies: DownloadDependencyRow[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadableDependencyFile {
|
||||||
|
href: string
|
||||||
|
filename: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectDownloadSelection {
|
||||||
|
currentGameVersion: string | null
|
||||||
|
currentPlatform: string | null
|
||||||
|
selectedVersion: Labrinth.Versions.v3.Version | null
|
||||||
|
selectedPrimaryFile: Labrinth.Versions.v3.VersionFile | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DownloadModalProviderOptions {
|
||||||
|
project: ComputedRef<DownloadModalProject | null>
|
||||||
|
selectedVersion: ComputedRef<Labrinth.Versions.v3.Version | null>
|
||||||
|
currentGameVersion: ComputedRef<string | null>
|
||||||
|
currentPlatform: ComputedRef<string | null>
|
||||||
|
downloadReason: ComputedRef<CdnDownloadReason>
|
||||||
|
additionalFiles: ComputedRef<Labrinth.Versions.v3.VersionFile[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadModalProvider {
|
||||||
|
visibleDependencyRows: ComputedRef<DownloadDependencyRow[]>
|
||||||
|
duplicateDependencyRowsHidden: ComputedRef<boolean>
|
||||||
|
downloadRows: ComputedRef<DownloadDependencyRow[]>
|
||||||
|
downloadableDependencyFiles: ComputedRef<DownloadableDependencyFile[]>
|
||||||
|
downloadableDependencyFilesLoaded: ComputedRef<boolean>
|
||||||
|
preloadDependenciesForSelection: (selection: ProjectDownloadSelection) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const [injectDownloadModalProvider, provideDownloadModalContext] =
|
||||||
|
createContext<DownloadModalProvider>('DownloadModal')
|
||||||
|
|
||||||
|
export function provideDownloadModalProvider(
|
||||||
|
options: DownloadModalProviderOptions,
|
||||||
|
): DownloadModalProvider {
|
||||||
|
const client = injectModrinthClient()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { createProjectDownloadUrl } = useCdnDownloadContext()
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const shouldResolveDependencies = computed(
|
||||||
|
() => !!options.project.value && !!options.selectedVersion.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
const dependencyResolutionPreferences = computed(() =>
|
||||||
|
createResolutionPreferences(options.selectedVersion.value, options.currentPlatform.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: dependencyResolution, isFetching: dependencyResolutionFetching } = useQuery({
|
||||||
|
...dependencyResolutionQueryOptions(
|
||||||
|
client,
|
||||||
|
options.project,
|
||||||
|
options.selectedVersion,
|
||||||
|
dependencyResolutionPreferences,
|
||||||
|
),
|
||||||
|
enabled: shouldResolveDependencies,
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleResolvedDependencies = computed<ResolvedContent[]>(() =>
|
||||||
|
visibleDependencies(dependencyResolution.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
const dependencyVersionIds = computed(() =>
|
||||||
|
sortedUnique(
|
||||||
|
visibleResolvedDependencies.value
|
||||||
|
.filter((dependency) => !('reason' in dependency))
|
||||||
|
.map((dependency) => dependency.version_id)
|
||||||
|
.filter((versionId): versionId is string => !!versionId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: dependencyVersions, isFetching: dependencyVersionsFetching } = useQuery({
|
||||||
|
...dependencyVersionsQueryOptions(client, dependencyVersionIds),
|
||||||
|
enabled: computed(
|
||||||
|
() => shouldResolveDependencies.value && dependencyVersionIds.value.length > 0,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const dependencyVersionById = computed(() => {
|
||||||
|
const map = new Map<string, Labrinth.Versions.v3.Version>()
|
||||||
|
for (const version of dependencyVersions.value || []) {
|
||||||
|
if (!version) continue
|
||||||
|
map.set(version.id, version)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
const dependencyProjectIds = computed(() =>
|
||||||
|
sortedUnique(
|
||||||
|
visibleResolvedDependencies.value
|
||||||
|
.map((dependency) => dependency.project_id)
|
||||||
|
.filter((projectId): projectId is string => !!projectId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: dependencyProjects, isFetching: dependencyProjectsFetching } = useQuery({
|
||||||
|
...dependencyProjectsQueryOptions(client, dependencyProjectIds),
|
||||||
|
enabled: computed(
|
||||||
|
() => shouldResolveDependencies.value && dependencyProjectIds.value.length > 0,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const dependencyProjectById = computed(() => {
|
||||||
|
const map = new Map<string, Labrinth.Projects.v2.Project>()
|
||||||
|
for (const project of dependencyProjects.value || []) {
|
||||||
|
map.set(project.id, project)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
const dependenciesByParentVersionId = computed(() => {
|
||||||
|
const map = new Map<string, ResolvedContent[]>()
|
||||||
|
|
||||||
|
for (const dependency of visibleResolvedDependencies.value) {
|
||||||
|
if (!dependency.dependent_on_version_id) continue
|
||||||
|
|
||||||
|
const dependencies = map.get(dependency.dependent_on_version_id) || []
|
||||||
|
dependencies.push(dependency)
|
||||||
|
map.set(dependency.dependent_on_version_id, dependencies)
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
const dependenciesLoaded = computed(() => {
|
||||||
|
if (!shouldResolveDependencies.value) return false
|
||||||
|
if (dependencyResolutionFetching.value) return false
|
||||||
|
if (!dependencyResolution.value) return false
|
||||||
|
if (
|
||||||
|
dependencyResolution.value.primary.version_id &&
|
||||||
|
dependencyResolution.value.primary.version_id !== options.selectedVersion.value?.id
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
dependencyVersionsFetching.value ||
|
||||||
|
!dependencyVersionIds.value.every((versionId) => dependencyVersionById.value.has(versionId))
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
dependencyProjectsFetching.value ||
|
||||||
|
!dependencyProjectIds.value.every((projectId) => dependencyProjectById.value.has(projectId))
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolvedDependencyRows = computed<DownloadDependencyRow[]>(() => {
|
||||||
|
if (!dependenciesLoaded.value) return []
|
||||||
|
|
||||||
|
const primaryVersionId =
|
||||||
|
dependencyResolution.value?.primary.version_id || options.selectedVersion.value?.id
|
||||||
|
if (!primaryVersionId) return []
|
||||||
|
|
||||||
|
const dependencies = dependenciesByParentVersionId.value.get(primaryVersionId) || []
|
||||||
|
|
||||||
|
return dependencies.flatMap((dependency) => {
|
||||||
|
const row = createDependencyRow(dependency)
|
||||||
|
return row ? [row] : []
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleDependencyRows = computed<DownloadDependencyRow[]>(() =>
|
||||||
|
dedupeDependencyRows(resolvedDependencyRows.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
const duplicateDependencyRowsHidden = computed(() =>
|
||||||
|
hasDuplicateDependencyRows(resolvedDependencyRows.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
const additionalFileRows = computed<DownloadDependencyRow[]>(() =>
|
||||||
|
options.additionalFiles.value.map((file) => ({
|
||||||
|
key: `additional-file-${additionalFileKey(file)}`,
|
||||||
|
name: file.filename,
|
||||||
|
fallbackIcon: FileIcon,
|
||||||
|
downloadHref: getDownloadUrl(file.url),
|
||||||
|
filename: file.filename,
|
||||||
|
fileSize: file.size,
|
||||||
|
typeLabel: fileTypeLabel(file.file_type),
|
||||||
|
unavailableTooltip: formatMessage(messages.unavailableFile),
|
||||||
|
dependencies: [],
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
|
||||||
|
const downloadRows = computed<DownloadDependencyRow[]>(() => [
|
||||||
|
...visibleDependencyRows.value,
|
||||||
|
...additionalFileRows.value,
|
||||||
|
])
|
||||||
|
|
||||||
|
const downloadableDependencyFiles = computed<DownloadableDependencyFile[]>(() =>
|
||||||
|
collectDownloadableDependencyFiles(visibleDependencyRows.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
const downloadableDependencyFilesLoaded = computed(() => {
|
||||||
|
if (!shouldResolveDependencies.value) return false
|
||||||
|
return dependenciesLoaded.value
|
||||||
|
})
|
||||||
|
|
||||||
|
async function preloadDependenciesForSelection(selection: ProjectDownloadSelection) {
|
||||||
|
if (!options.project.value || !selection.selectedVersion) return
|
||||||
|
|
||||||
|
const preferences = createResolutionPreferences(
|
||||||
|
selection.selectedVersion,
|
||||||
|
selection.currentPlatform,
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolution = await queryClient.ensureQueryData({
|
||||||
|
queryKey: [
|
||||||
|
'project-download-modal',
|
||||||
|
'content-resolve',
|
||||||
|
options.project.value.id,
|
||||||
|
selection.selectedVersion.id,
|
||||||
|
options.project.value.project_type,
|
||||||
|
preferences,
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
client.labrinth.content_v3.resolve({
|
||||||
|
project_id: options.project.value!.id,
|
||||||
|
version_id: selection.selectedVersion!.id,
|
||||||
|
content_type: resolveContentType(options.project.value!.project_type),
|
||||||
|
selected: preferences,
|
||||||
|
target: preferences,
|
||||||
|
}),
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
})
|
||||||
|
const visible = visibleDependencies(resolution)
|
||||||
|
const versionIds = getDependencyVersionIds(visible)
|
||||||
|
const projectIds = getDependencyProjectIds(visible)
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
versionIds.length > 0
|
||||||
|
? queryClient.ensureQueryData({
|
||||||
|
queryKey: ['project-download-modal', 'resolved-versions', versionIds],
|
||||||
|
queryFn: () => client.labrinth.versions_v3.getVersions(versionIds),
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
})
|
||||||
|
: Promise.resolve(),
|
||||||
|
projectIds.length > 0
|
||||||
|
? queryClient.ensureQueryData({
|
||||||
|
queryKey: ['project-download-modal', 'resolved-projects', projectIds],
|
||||||
|
queryFn: () => client.labrinth.projects_v2.getMultiple(projectIds),
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
})
|
||||||
|
: Promise.resolve(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDependencyRow(dependency: ResolvedContent): DownloadDependencyRow | null {
|
||||||
|
const versionId = dependency.version_id ?? undefined
|
||||||
|
const version = versionId ? dependencyVersionById.value.get(versionId) : undefined
|
||||||
|
const project = dependencyProjectById.value.get(dependency.project_id)
|
||||||
|
if (!project) return null
|
||||||
|
|
||||||
|
const primaryFile = primaryFileForVersion(version)
|
||||||
|
const unavailableTooltip =
|
||||||
|
'reason' in dependency && dependency.reason
|
||||||
|
? skippedReasonLabel(dependency.reason)
|
||||||
|
: formatMessage(messages.unavailableDependency)
|
||||||
|
const name = project.title
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: `${dependency.project_id}-${versionId ?? 'unresolved'}-${
|
||||||
|
'reason' in dependency ? dependency.reason : 'resolved'
|
||||||
|
}`,
|
||||||
|
name,
|
||||||
|
icon: project.icon_url ?? undefined,
|
||||||
|
projectHref: `/${project.project_type}/${project.slug || project.id}`,
|
||||||
|
downloadHref:
|
||||||
|
'reason' in dependency || !primaryFile ? undefined : getDownloadUrl(primaryFile.url),
|
||||||
|
filename: primaryFile?.filename,
|
||||||
|
fileSize: primaryFile?.size,
|
||||||
|
typeLabel: 'Required',
|
||||||
|
unavailableTooltip,
|
||||||
|
dependencies: (versionId && dependenciesByParentVersionId.value.get(versionId)
|
||||||
|
? dependenciesByParentVersionId.value.get(versionId)!
|
||||||
|
: []
|
||||||
|
).flatMap((subDependency) => {
|
||||||
|
const row = createDependencyRow(subDependency)
|
||||||
|
return row ? [row] : []
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function skippedReasonLabel(reason: Labrinth.Content.v3.SkippedContent['reason']) {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
already_installed: formatMessage(messages.alreadyInstalledDependency),
|
||||||
|
duplicate_project: formatMessage(messages.duplicateDependency),
|
||||||
|
conflicting_dependency: formatMessage(messages.conflictingDependency),
|
||||||
|
no_compatible_version: formatMessage(messages.noCompatibleDependency),
|
||||||
|
missing_version: formatMessage(messages.missingDependencyVersion),
|
||||||
|
quilt_fabric_api: formatMessage(messages.quiltFabricApiDependency),
|
||||||
|
}[reason] || formatMessage(messages.unavailableDependency)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadUrl(url: string) {
|
||||||
|
return createProjectDownloadUrl(url, {
|
||||||
|
reason: options.downloadReason.value,
|
||||||
|
gameVersion: options.currentGameVersion.value ?? undefined,
|
||||||
|
loader: options.currentPlatform.value ?? undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileTypeLabel(type?: Labrinth.Versions.v3.FileType | null) {
|
||||||
|
return formatMessage(fileTypeMessages[type ?? 'unknown'] ?? fileTypeMessages.unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = {
|
||||||
|
visibleDependencyRows,
|
||||||
|
duplicateDependencyRowsHidden,
|
||||||
|
downloadRows,
|
||||||
|
downloadableDependencyFiles,
|
||||||
|
downloadableDependencyFilesLoaded,
|
||||||
|
preloadDependenciesForSelection,
|
||||||
|
}
|
||||||
|
|
||||||
|
provideDownloadModalContext(provider)
|
||||||
|
|
||||||
|
return provider
|
||||||
|
}
|
||||||
|
|
||||||
|
function dependencyResolutionQueryOptions(
|
||||||
|
client: AbstractModrinthClient,
|
||||||
|
project: ComputedRef<DownloadModalProject | null>,
|
||||||
|
selectedVersion: ComputedRef<Labrinth.Versions.v3.Version | null>,
|
||||||
|
preferences: ComputedRef<Labrinth.Content.v3.ResolutionPreferences>,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
queryKey: computed(() => [
|
||||||
|
'project-download-modal',
|
||||||
|
'content-resolve',
|
||||||
|
project.value?.id,
|
||||||
|
selectedVersion.value?.id,
|
||||||
|
project.value?.project_type,
|
||||||
|
preferences.value,
|
||||||
|
]),
|
||||||
|
queryFn: () =>
|
||||||
|
client.labrinth.content_v3.resolve({
|
||||||
|
project_id: project.value!.id,
|
||||||
|
version_id: selectedVersion.value!.id,
|
||||||
|
content_type: resolveContentType(project.value!.project_type),
|
||||||
|
selected: preferences.value,
|
||||||
|
target: preferences.value,
|
||||||
|
}),
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dependencyVersionsQueryOptions(
|
||||||
|
client: AbstractModrinthClient,
|
||||||
|
versionIds: ComputedRef<string[]>,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
queryKey: computed(() => ['project-download-modal', 'resolved-versions', versionIds.value]),
|
||||||
|
queryFn: () => client.labrinth.versions_v3.getVersions(versionIds.value),
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dependencyProjectsQueryOptions(
|
||||||
|
client: AbstractModrinthClient,
|
||||||
|
projectIds: ComputedRef<string[]>,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
queryKey: computed(() => ['project-download-modal', 'resolved-projects', projectIds.value]),
|
||||||
|
queryFn: () => client.labrinth.projects_v2.getMultiple(projectIds.value),
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createResolutionPreferences(
|
||||||
|
version: Labrinth.Versions.v3.Version | null,
|
||||||
|
currentPlatform: string | null,
|
||||||
|
): Labrinth.Content.v3.ResolutionPreferences {
|
||||||
|
return {
|
||||||
|
game_versions: version?.game_versions || [],
|
||||||
|
loaders: currentPlatform ? [currentPlatform] : version?.loaders || [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleDependencies(resolution?: Labrinth.Content.v3.ResolveContentPlan) {
|
||||||
|
return [...(resolution?.dependencies || []), ...(resolution?.skipped || [])].filter(
|
||||||
|
shouldShowDependency,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDependencyVersionIds(dependencies: ResolvedContent[]) {
|
||||||
|
return sortedUnique(
|
||||||
|
dependencies
|
||||||
|
.filter((dependency) => !('reason' in dependency))
|
||||||
|
.map((dependency) => dependency.version_id)
|
||||||
|
.filter((versionId): versionId is string => !!versionId),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDependencyProjectIds(dependencies: ResolvedContent[]) {
|
||||||
|
return sortedUnique(
|
||||||
|
dependencies
|
||||||
|
.map((dependency) => dependency.project_id)
|
||||||
|
.filter((projectId): projectId is string => !!projectId),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function primaryFileForVersion(version?: Labrinth.Versions.v3.Version) {
|
||||||
|
return version?.files?.find((file) => file.primary) || version?.files?.[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShowDependency(dependency: ResolvedContent) {
|
||||||
|
return !(
|
||||||
|
'reason' in dependency && ['duplicate_project', 'quilt_fabric_api'].includes(dependency.reason)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveContentType(projectType: DisplayProjectType): Labrinth.Content.v3.ContentType {
|
||||||
|
return ['mod', 'plugin', 'datapack', 'resourcepack', 'shader', 'modpack'].includes(projectType)
|
||||||
|
? (projectType as Labrinth.Content.v3.ContentType)
|
||||||
|
: 'mod'
|
||||||
|
}
|
||||||
|
|
||||||
|
function additionalFileKey(file: Labrinth.Versions.v3.VersionFile) {
|
||||||
|
return file.hashes?.sha1 ?? file.filename
|
||||||
|
}
|
||||||
|
|
||||||
|
function dedupeDependencyRows(
|
||||||
|
rows: DownloadDependencyRow[],
|
||||||
|
seenDependencies = new Set<string>(),
|
||||||
|
): DownloadDependencyRow[] {
|
||||||
|
return rows.flatMap((row) => {
|
||||||
|
const identity = dependencyRowIdentity(row)
|
||||||
|
if (seenDependencies.has(identity)) return []
|
||||||
|
|
||||||
|
seenDependencies.add(identity)
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...row,
|
||||||
|
dependencies: dedupeDependencyRows(row.dependencies, seenDependencies),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function dependencyRowIdentity(row: DownloadDependencyRow) {
|
||||||
|
return row.projectHref ?? row.downloadHref ?? row.key
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDuplicateDependencyRows(
|
||||||
|
rows: DownloadDependencyRow[],
|
||||||
|
seenDependencies = new Set<string>(),
|
||||||
|
): boolean {
|
||||||
|
for (const row of rows) {
|
||||||
|
const rowId = dependencyRowIdentity(row)
|
||||||
|
if (seenDependencies.has(rowId)) return true
|
||||||
|
seenDependencies.add(rowId)
|
||||||
|
if (hasDuplicateDependencyRows(row.dependencies, seenDependencies)) return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectDownloadableDependencyFiles(
|
||||||
|
rows: DownloadDependencyRow[],
|
||||||
|
seenHrefs = new Set<string>(),
|
||||||
|
): DownloadableDependencyFile[] {
|
||||||
|
const files: DownloadableDependencyFile[] = []
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row.downloadHref && !seenHrefs.has(row.downloadHref)) {
|
||||||
|
seenHrefs.add(row.downloadHref)
|
||||||
|
files.push({
|
||||||
|
href: row.downloadHref,
|
||||||
|
filename: row.filename || filenameFromUrl(row.downloadHref),
|
||||||
|
name: row.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push(...collectDownloadableDependencyFiles(row.dependencies, seenHrefs))
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
function filenameFromUrl(url: string) {
|
||||||
|
try {
|
||||||
|
const filename = new URL(url).pathname.split('/').pop()
|
||||||
|
return filename ? decodeURIComponent(filename) : 'dependency.jar'
|
||||||
|
} catch {
|
||||||
|
return 'dependency.jar'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortedUnique(values: string[]) {
|
||||||
|
return [...new Set(values)].sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
alreadyInstalledDependency: {
|
||||||
|
id: 'project.download.dependency-already-installed',
|
||||||
|
defaultMessage: 'This dependency is already installed',
|
||||||
|
},
|
||||||
|
conflictingDependency: {
|
||||||
|
id: 'project.download.dependency-conflicting',
|
||||||
|
defaultMessage: 'This dependency conflicts with another dependency',
|
||||||
|
},
|
||||||
|
duplicateDependency: {
|
||||||
|
id: 'project.download.dependency-duplicate',
|
||||||
|
defaultMessage: 'This dependency is already included',
|
||||||
|
},
|
||||||
|
missingDependencyVersion: {
|
||||||
|
id: 'project.download.dependency-missing-version',
|
||||||
|
defaultMessage: 'This dependency version is unavailable',
|
||||||
|
},
|
||||||
|
noCompatibleDependency: {
|
||||||
|
id: 'project.download.dependency-no-compatible-version',
|
||||||
|
defaultMessage: 'No compatible version is available for this dependency',
|
||||||
|
},
|
||||||
|
quiltFabricApiDependency: {
|
||||||
|
id: 'project.download.dependency-quilt-fabric-api',
|
||||||
|
defaultMessage: 'Fabric API is skipped for Quilt',
|
||||||
|
},
|
||||||
|
unavailableDependency: {
|
||||||
|
id: 'project.download.dependency-unavailable',
|
||||||
|
defaultMessage: 'This dependency cannot be downloaded',
|
||||||
|
},
|
||||||
|
unavailableFile: {
|
||||||
|
id: 'project.download.file-unavailable',
|
||||||
|
defaultMessage: 'This file cannot be downloaded',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -31,17 +31,7 @@
|
|||||||
@download="onDownload"
|
@download="onDownload"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<DownloadDependencies
|
<DownloadDependencies @download="onDownload" />
|
||||||
:project="project"
|
|
||||||
:selected-version="selectedVersion"
|
|
||||||
:current-game-version="currentGameVersion"
|
|
||||||
:current-platform="currentPlatform"
|
|
||||||
:download-reason="downloadReason"
|
|
||||||
:additional-files="additionalFiles"
|
|
||||||
@update:downloadable-files="dependencyDownloadFiles = $event"
|
|
||||||
@update:downloadable-files-loaded="dependencyDownloadFilesLoaded = $event"
|
|
||||||
@download="onDownload"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<ServersPromo
|
<ServersPromo
|
||||||
v-if="flags.showProjectPageDownloadModalServersPromo"
|
v-if="flags.showProjectPageDownloadModalServersPromo"
|
||||||
@@ -109,7 +99,8 @@ import {
|
|||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import type { DisplayProjectType } from '@modrinth/utils'
|
import type { DisplayProjectType } from '@modrinth/utils'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
@@ -120,6 +111,7 @@ import { STALE_TIME, STALE_TIME_LONG } from '~/composables/queries/project'
|
|||||||
import DownloadDependencies from './DownloadDependencies.vue'
|
import DownloadDependencies from './DownloadDependencies.vue'
|
||||||
import DownloadProject from './DownloadProject.vue'
|
import DownloadProject from './DownloadProject.vue'
|
||||||
import InstallWithModrinthApp from './InstallWithModrinthApp.vue'
|
import InstallWithModrinthApp from './InstallWithModrinthApp.vue'
|
||||||
|
import { provideDownloadModalProvider } from './download-modal-provider'
|
||||||
|
|
||||||
type DownloadModalProject = Omit<Labrinth.Projects.v2.Project, 'project_type'> & {
|
type DownloadModalProject = Omit<Labrinth.Projects.v2.Project, 'project_type'> & {
|
||||||
project_type: DisplayProjectType
|
project_type: DisplayProjectType
|
||||||
@@ -183,6 +175,7 @@ const route = useRoute()
|
|||||||
const flags = useFeatureFlags()
|
const flags = useFeatureFlags()
|
||||||
const tags = useGeneratedState()
|
const tags = useGeneratedState()
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const { createProjectDownloadUrl } = useCdnDownloadContext()
|
const { createProjectDownloadUrl } = useCdnDownloadContext()
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
@@ -190,6 +183,7 @@ const debug = useDebugLogger('DownloadModal')
|
|||||||
|
|
||||||
const modal = ref<NewModalRef | null>(null)
|
const modal = ref<NewModalRef | null>(null)
|
||||||
const downloadTitleRef = ref<HTMLElement | null>(null)
|
const downloadTitleRef = ref<HTMLElement | null>(null)
|
||||||
|
const modalOpening = ref(false)
|
||||||
const modalOpen = ref(false)
|
const modalOpen = ref(false)
|
||||||
const showProjectId = ref<string | null>(null)
|
const showProjectId = ref<string | null>(null)
|
||||||
const showOptions = ref<ResolvedProjectDownloadModalShowOptions>(getDefaultShowOptions())
|
const showOptions = ref<ResolvedProjectDownloadModalShowOptions>(getDefaultShowOptions())
|
||||||
@@ -199,13 +193,13 @@ const pendingRouteSelection = ref({
|
|||||||
gameVersion: getStringQueryValue(route.query.version),
|
gameVersion: getStringQueryValue(route.query.version),
|
||||||
platform: getStringQueryValue(route.query.loader),
|
platform: getStringQueryValue(route.query.loader),
|
||||||
})
|
})
|
||||||
const dependencyDownloadFiles = ref<DownloadableFile[]>([])
|
|
||||||
const dependencyDownloadFilesLoaded = ref(false)
|
|
||||||
const downloadingActionType = ref<DownloadActionType | null>(null)
|
const downloadingActionType = ref<DownloadActionType | null>(null)
|
||||||
const MODAL_CLOSE_STATE_RESET_MS = 350
|
const MODAL_CLOSE_STATE_RESET_MS = 350
|
||||||
const DOWNLOAD_URL_REVOKE_MS = 60000
|
const DOWNLOAD_URL_REVOKE_MS = 60000
|
||||||
const DOWNLOAD_STAGGER_MS = 500
|
const DOWNLOAD_STAGGER_MS = 500
|
||||||
let closeStateResetTimeout: ReturnType<typeof setTimeout> | null = null
|
let closeStateResetTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let modalShowRequestId = 0
|
||||||
|
let unmounted = false
|
||||||
|
|
||||||
const routeProjectId = computed(() => showProjectId.value ?? props.projectId ?? null)
|
const routeProjectId = computed(() => showProjectId.value ?? props.projectId ?? null)
|
||||||
|
|
||||||
@@ -249,24 +243,9 @@ const { data: versionsV3, isFetching: versionsV3Loading } = useQuery({
|
|||||||
enabled: computed(() => !!resolvedProjectId.value && versionsEnabled.value),
|
enabled: computed(() => !!resolvedProjectId.value && versionsEnabled.value),
|
||||||
})
|
})
|
||||||
|
|
||||||
const versions = computed<Labrinth.Versions.v3.Version[]>(() => {
|
const versions = computed<Labrinth.Versions.v3.Version[]>(() =>
|
||||||
const isModpack =
|
normalizeVersionsForDownload(versionsV3.value ?? []),
|
||||||
project.value?.actualProjectType === 'modpack' || project.value?.project_type === 'modpack'
|
)
|
||||||
|
|
||||||
return (versionsV3.value ?? []).map((version) => {
|
|
||||||
const files = Array.isArray(version.files) ? version.files : []
|
|
||||||
const gameVersions = Array.isArray(version.game_versions) ? version.game_versions : []
|
|
||||||
const loaders = Array.isArray(version.loaders) ? version.loaders : []
|
|
||||||
const mrpackLoaders = Array.isArray(version.mrpack_loaders) ? version.mrpack_loaders : []
|
|
||||||
|
|
||||||
return {
|
|
||||||
...version,
|
|
||||||
files,
|
|
||||||
game_versions: gameVersions,
|
|
||||||
loaders: isModpack && mrpackLoaders.length ? mrpackLoaders : loaders,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const initialGameVersion = computed(() => {
|
const initialGameVersion = computed(() => {
|
||||||
const version = route.query.version
|
const version = route.query.version
|
||||||
@@ -289,6 +268,17 @@ const additionalFiles = computed(() => {
|
|||||||
return selectedVersion.value.files.filter((file) => file !== selectedPrimaryFile.value)
|
return selectedVersion.value.files.filter((file) => file !== selectedPrimaryFile.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const downloadModalProvider = provideDownloadModalProvider({
|
||||||
|
project,
|
||||||
|
selectedVersion,
|
||||||
|
currentGameVersion,
|
||||||
|
currentPlatform,
|
||||||
|
downloadReason: computed(() => props.downloadReason),
|
||||||
|
additionalFiles,
|
||||||
|
})
|
||||||
|
const dependencyDownloadFiles = downloadModalProvider.downloadableDependencyFiles
|
||||||
|
const dependencyDownloadFilesLoaded = downloadModalProvider.downloadableDependencyFilesLoaded
|
||||||
|
|
||||||
const selectedVersionDownloadFiles = computed<DownloadableFile[]>(() => {
|
const selectedVersionDownloadFiles = computed<DownloadableFile[]>(() => {
|
||||||
if (!selectedVersion.value) return []
|
if (!selectedVersion.value) return []
|
||||||
|
|
||||||
@@ -406,16 +396,6 @@ function selectPlatform(platform: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateProjectDownloadSelection(selection: ProjectDownloadSelection) {
|
function updateProjectDownloadSelection(selection: ProjectDownloadSelection) {
|
||||||
const previousSelection = projectDownloadSelection.value
|
|
||||||
if (
|
|
||||||
previousSelection.selectedVersion?.id !== selection.selectedVersion?.id ||
|
|
||||||
previousSelection.currentGameVersion !== selection.currentGameVersion ||
|
|
||||||
previousSelection.currentPlatform !== selection.currentPlatform
|
|
||||||
) {
|
|
||||||
dependencyDownloadFiles.value = []
|
|
||||||
dependencyDownloadFilesLoaded.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
projectDownloadSelection.value = selection
|
projectDownloadSelection.value = selection
|
||||||
pendingRouteSelection.value = {
|
pendingRouteSelection.value = {
|
||||||
gameVersion: selection.currentGameVersion,
|
gameVersion: selection.currentGameVersion,
|
||||||
@@ -451,22 +431,37 @@ async function show(
|
|||||||
event?: MouseEvent,
|
event?: MouseEvent,
|
||||||
options: ProjectDownloadModalShowOptions = {},
|
options: ProjectDownloadModalShowOptions = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!modal.value || modalOpen.value) return
|
if (!modal.value || modalOpening.value || modalOpen.value) return
|
||||||
await waitForCloseStateReset()
|
const showRequestId = ++modalShowRequestId
|
||||||
if (!modal.value || modalOpen.value) return
|
modalOpening.value = true
|
||||||
showOptions.value = {
|
|
||||||
...getDefaultShowOptions(),
|
try {
|
||||||
...options,
|
await waitForCloseStateReset()
|
||||||
|
if (!isActiveShowRequest(showRequestId)) return
|
||||||
|
showOptions.value = {
|
||||||
|
...getDefaultShowOptions(),
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
showProjectId.value = showOptions.value.projectId ?? null
|
||||||
|
await nextTick()
|
||||||
|
if (!isActiveShowRequest(showRequestId)) return
|
||||||
|
if (!(await loadProjectForModal(!!showOptions.value.projectId))) return
|
||||||
|
if (!isActiveShowRequest(showRequestId)) return
|
||||||
|
resetDownloadState()
|
||||||
|
await preloadRouteSelectedDownload()
|
||||||
|
if (!isActiveShowRequest(showRequestId)) return
|
||||||
|
modalOpen.value = true
|
||||||
|
modal.value.show(event)
|
||||||
|
} finally {
|
||||||
|
if (modalShowRequestId === showRequestId) {
|
||||||
|
modalOpening.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
showProjectId.value = showOptions.value.projectId ?? null
|
|
||||||
await nextTick()
|
|
||||||
if (!(await loadProjectForModal(!!showOptions.value.projectId))) return
|
|
||||||
resetDownloadState()
|
|
||||||
modalOpen.value = true
|
|
||||||
modal.value.show(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
|
modalShowRequestId += 1
|
||||||
|
modalOpening.value = false
|
||||||
if (!modal.value || !modalOpen.value) return
|
if (!modal.value || !modalOpen.value) return
|
||||||
modal.value?.hide()
|
modal.value?.hide()
|
||||||
}
|
}
|
||||||
@@ -661,6 +656,15 @@ function getStringQueryValue(value: unknown) {
|
|||||||
return typeof value === 'string' ? value : null
|
return typeof value === 'string' ? value : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldPreloadRouteSelectedDownload() {
|
||||||
|
return (
|
||||||
|
props.useRouteHash &&
|
||||||
|
!showOptions.value.projectId &&
|
||||||
|
!!getStringQueryValue(route.query.version) &&
|
||||||
|
!!getStringQueryValue(route.query.loader)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function clearCloseStateResetTimeout() {
|
function clearCloseStateResetTimeout() {
|
||||||
if (!closeStateResetTimeout) return
|
if (!closeStateResetTimeout) return
|
||||||
clearTimeout(closeStateResetTimeout)
|
clearTimeout(closeStateResetTimeout)
|
||||||
@@ -684,10 +688,133 @@ async function loadProjectForModal(forceRefetch: boolean) {
|
|||||||
return !!data
|
return !!data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadVersionsForModal() {
|
||||||
|
if (!resolvedProjectId.value) return null
|
||||||
|
versionsEnabled.value = true
|
||||||
|
if (versionsV3.value) return versions.value
|
||||||
|
|
||||||
|
const data = await queryClient.ensureQueryData({
|
||||||
|
queryKey: ['project', resolvedProjectId.value, 'versions', 'v3'],
|
||||||
|
queryFn: () =>
|
||||||
|
client.labrinth.versions_v3.getProjectVersions(resolvedProjectId.value!, {
|
||||||
|
include_changelog: false,
|
||||||
|
apiVersion: 3,
|
||||||
|
}),
|
||||||
|
staleTime: STALE_TIME_LONG,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.isArray(data) ? normalizeVersionsForDownload(data) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preloadRouteSelectedDownload() {
|
||||||
|
if (!shouldPreloadRouteSelectedDownload()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const routeVersions = await loadVersionsForModal()
|
||||||
|
if (!routeVersions) return
|
||||||
|
|
||||||
|
const selection = getRouteSelectedDownloadSelection(routeVersions)
|
||||||
|
if (!selection) return
|
||||||
|
|
||||||
|
await preloadDependenciesForSelection(selection)
|
||||||
|
} catch (error) {
|
||||||
|
debug('failed to preload selected route download', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRouteSelectedDownloadSelection(
|
||||||
|
versionList: Labrinth.Versions.v3.Version[] = versions.value,
|
||||||
|
): ProjectDownloadSelection | null {
|
||||||
|
const gameVersion = initialGameVersion.value
|
||||||
|
const platform = initialPlatform.value
|
||||||
|
const version = getSelectedRouteVersion(gameVersion, platform, versionList)
|
||||||
|
const primaryFile =
|
||||||
|
version?.files?.find((file) => file.primary) || version?.files?.[0] || null
|
||||||
|
|
||||||
|
if (!gameVersion || !platform || !version || !primaryFile) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentGameVersion: gameVersion,
|
||||||
|
currentPlatform: platform,
|
||||||
|
selectedVersion: version,
|
||||||
|
selectedPrimaryFile: primaryFile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedRouteVersion(
|
||||||
|
gameVersion: string | null,
|
||||||
|
platform: string | null,
|
||||||
|
versionList: Labrinth.Versions.v3.Version[],
|
||||||
|
) {
|
||||||
|
if (!gameVersion || !platform || !project.value) return null
|
||||||
|
|
||||||
|
const filteredVersions = versionList.filter((version) => {
|
||||||
|
const matchesPlatform =
|
||||||
|
project.value?.project_type === 'resourcepack' ||
|
||||||
|
(!!platform && version.loaders.includes(platform))
|
||||||
|
|
||||||
|
return version.game_versions.includes(gameVersion) && matchesPlatform
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
latestVersionByType(filteredVersions, 'release') ||
|
||||||
|
latestVersionByType(filteredVersions, 'beta') ||
|
||||||
|
latestVersionByType(filteredVersions, 'alpha') ||
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeVersionsForDownload(
|
||||||
|
versionList: Labrinth.Versions.v3.Version[],
|
||||||
|
): Labrinth.Versions.v3.Version[] {
|
||||||
|
const isModpack =
|
||||||
|
project.value?.actualProjectType === 'modpack' || project.value?.project_type === 'modpack'
|
||||||
|
|
||||||
|
return versionList.map((version) => {
|
||||||
|
const files = Array.isArray(version.files) ? version.files : []
|
||||||
|
const gameVersions = Array.isArray(version.game_versions) ? version.game_versions : []
|
||||||
|
const loaders = Array.isArray(version.loaders) ? version.loaders : []
|
||||||
|
const mrpackLoaders = Array.isArray(version.mrpack_loaders) ? version.mrpack_loaders : []
|
||||||
|
|
||||||
|
return {
|
||||||
|
...version,
|
||||||
|
files,
|
||||||
|
game_versions: gameVersions,
|
||||||
|
loaders: isModpack && mrpackLoaders.length ? mrpackLoaders : loaders,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestVersionByType(
|
||||||
|
versionList: Labrinth.Versions.v3.Version[],
|
||||||
|
type: Labrinth.Versions.v3.VersionChannel,
|
||||||
|
) {
|
||||||
|
return versionList
|
||||||
|
.filter((version) => version.version_type === type)
|
||||||
|
.reduce<Labrinth.Versions.v3.Version | undefined>((latest, version) => {
|
||||||
|
if (!latest || dayjs(version.date_published).isAfter(dayjs(latest.date_published))) {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
return latest
|
||||||
|
}, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function preloadDependenciesForSelection(selection: ProjectDownloadSelection) {
|
||||||
|
await downloadModalProvider.preloadDependenciesForSelection(selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActiveShowRequest(showRequestId: number) {
|
||||||
|
return (
|
||||||
|
!unmounted &&
|
||||||
|
modalShowRequestId === showRequestId &&
|
||||||
|
!!modal.value &&
|
||||||
|
!modalOpen.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function resetDownloadState() {
|
function resetDownloadState() {
|
||||||
projectDownloadSelection.value = getDefaultProjectDownloadSelection()
|
projectDownloadSelection.value = getDefaultProjectDownloadSelection()
|
||||||
dependencyDownloadFiles.value = []
|
|
||||||
dependencyDownloadFilesLoaded.value = false
|
|
||||||
downloadProjectResetKey.value += 1
|
downloadProjectResetKey.value += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,6 +822,7 @@ function openFromHash() {
|
|||||||
if (
|
if (
|
||||||
!props.useRouteHash ||
|
!props.useRouteHash ||
|
||||||
!modal.value ||
|
!modal.value ||
|
||||||
|
modalOpening.value ||
|
||||||
modalOpen.value ||
|
modalOpen.value ||
|
||||||
showProjectId.value ||
|
showProjectId.value ||
|
||||||
route.hash !== '#download'
|
route.hash !== '#download'
|
||||||
@@ -725,12 +853,14 @@ watch(modal, openFromHash)
|
|||||||
watch(() => route.hash, openFromHash)
|
watch(() => route.hash, openFromHash)
|
||||||
watch(routeProjectId, () => {
|
watch(routeProjectId, () => {
|
||||||
projectDownloadSelection.value = getDefaultProjectDownloadSelection()
|
projectDownloadSelection.value = getDefaultProjectDownloadSelection()
|
||||||
dependencyDownloadFiles.value = []
|
|
||||||
dependencyDownloadFilesLoaded.value = false
|
|
||||||
downloadProjectResetKey.value += 1
|
downloadProjectResetKey.value += 1
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(clearCloseStateResetTimeout)
|
onUnmounted(() => {
|
||||||
|
unmounted = true
|
||||||
|
modalShowRequestId += 1
|
||||||
|
clearCloseStateResetTimeout()
|
||||||
|
})
|
||||||
|
|
||||||
defineExpose({ show, hide })
|
defineExpose({ show, hide })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user