mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
refactor: implement download modal provider
This commit is contained in:
@@ -23,262 +23,35 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { FileIcon, InfoIcon } from '@modrinth/assets'
|
||||
import {
|
||||
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 { InfoIcon } from '@modrinth/assets'
|
||||
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import DownloadDependency from './DownloadDependency.vue'
|
||||
import { injectDownloadModalProvider } from './download-modal-provider'
|
||||
|
||||
defineOptions({
|
||||
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(
|
||||
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
|
||||
}>(),
|
||||
{
|
||||
dependencies: null,
|
||||
project: null,
|
||||
selectedVersion: null,
|
||||
currentGameVersion: null,
|
||||
currentPlatform: null,
|
||||
downloadReason: 'standalone',
|
||||
additionalFiles: () => [],
|
||||
showTitle: true,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
download: []
|
||||
'update:downloadable-files': [files: DownloadableDependencyFile[]]
|
||||
'update:downloadable-files-loaded': [loaded: boolean]
|
||||
}>()
|
||||
const client = injectModrinthClient()
|
||||
const { createProjectDownloadUrl } = useCdnDownloadContext()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const shouldResolveDependencies = computed(
|
||||
() => !props.dependencies && !!props.project && !!props.selectedVersion,
|
||||
)
|
||||
|
||||
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 {
|
||||
visibleDependencyRows,
|
||||
duplicateDependencyRowsHidden,
|
||||
downloadRows,
|
||||
} = injectDownloadModalProvider()
|
||||
|
||||
const sectionTitle = computed(() =>
|
||||
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({
|
||||
dependenciesTitle: {
|
||||
id: 'project.download.dependencies-title',
|
||||
@@ -476,37 +74,5 @@ const messages = defineMessages({
|
||||
id: 'project.download.additional-files-title',
|
||||
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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<div class="flex flex-col gap-4">
|
||||
<DownloadDependencies
|
||||
: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"
|
||||
/>
|
||||
<DownloadDependencies @download="onDownload" />
|
||||
</div>
|
||||
<ServersPromo
|
||||
v-if="flags.showProjectPageDownloadModalServersPromo"
|
||||
@@ -109,7 +99,8 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
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 { 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 DownloadProject from './DownloadProject.vue'
|
||||
import InstallWithModrinthApp from './InstallWithModrinthApp.vue'
|
||||
import { provideDownloadModalProvider } from './download-modal-provider'
|
||||
|
||||
type DownloadModalProject = Omit<Labrinth.Projects.v2.Project, 'project_type'> & {
|
||||
project_type: DisplayProjectType
|
||||
@@ -183,6 +175,7 @@ const route = useRoute()
|
||||
const flags = useFeatureFlags()
|
||||
const tags = useGeneratedState()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const { createProjectDownloadUrl } = useCdnDownloadContext()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -190,6 +183,7 @@ const debug = useDebugLogger('DownloadModal')
|
||||
|
||||
const modal = ref<NewModalRef | null>(null)
|
||||
const downloadTitleRef = ref<HTMLElement | null>(null)
|
||||
const modalOpening = ref(false)
|
||||
const modalOpen = ref(false)
|
||||
const showProjectId = ref<string | null>(null)
|
||||
const showOptions = ref<ResolvedProjectDownloadModalShowOptions>(getDefaultShowOptions())
|
||||
@@ -199,13 +193,13 @@ const pendingRouteSelection = ref({
|
||||
gameVersion: getStringQueryValue(route.query.version),
|
||||
platform: getStringQueryValue(route.query.loader),
|
||||
})
|
||||
const dependencyDownloadFiles = ref<DownloadableFile[]>([])
|
||||
const dependencyDownloadFilesLoaded = ref(false)
|
||||
const downloadingActionType = ref<DownloadActionType | null>(null)
|
||||
const MODAL_CLOSE_STATE_RESET_MS = 350
|
||||
const DOWNLOAD_URL_REVOKE_MS = 60000
|
||||
const DOWNLOAD_STAGGER_MS = 500
|
||||
let closeStateResetTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
let modalShowRequestId = 0
|
||||
let unmounted = false
|
||||
|
||||
const routeProjectId = computed(() => showProjectId.value ?? props.projectId ?? null)
|
||||
|
||||
@@ -249,24 +243,9 @@ const { data: versionsV3, isFetching: versionsV3Loading } = useQuery({
|
||||
enabled: computed(() => !!resolvedProjectId.value && versionsEnabled.value),
|
||||
})
|
||||
|
||||
const versions = computed<Labrinth.Versions.v3.Version[]>(() => {
|
||||
const isModpack =
|
||||
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 versions = computed<Labrinth.Versions.v3.Version[]>(() =>
|
||||
normalizeVersionsForDownload(versionsV3.value ?? []),
|
||||
)
|
||||
|
||||
const initialGameVersion = computed(() => {
|
||||
const version = route.query.version
|
||||
@@ -289,6 +268,17 @@ const additionalFiles = computed(() => {
|
||||
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[]>(() => {
|
||||
if (!selectedVersion.value) return []
|
||||
|
||||
@@ -406,16 +396,6 @@ function selectPlatform(platform: string) {
|
||||
}
|
||||
|
||||
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
|
||||
pendingRouteSelection.value = {
|
||||
gameVersion: selection.currentGameVersion,
|
||||
@@ -451,22 +431,37 @@ async function show(
|
||||
event?: MouseEvent,
|
||||
options: ProjectDownloadModalShowOptions = {},
|
||||
): Promise<void> {
|
||||
if (!modal.value || modalOpen.value) return
|
||||
await waitForCloseStateReset()
|
||||
if (!modal.value || modalOpen.value) return
|
||||
showOptions.value = {
|
||||
...getDefaultShowOptions(),
|
||||
...options,
|
||||
if (!modal.value || modalOpening.value || modalOpen.value) return
|
||||
const showRequestId = ++modalShowRequestId
|
||||
modalOpening.value = true
|
||||
|
||||
try {
|
||||
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() {
|
||||
modalShowRequestId += 1
|
||||
modalOpening.value = false
|
||||
if (!modal.value || !modalOpen.value) return
|
||||
modal.value?.hide()
|
||||
}
|
||||
@@ -661,6 +656,15 @@ function getStringQueryValue(value: unknown) {
|
||||
return typeof value === 'string' ? value : null
|
||||
}
|
||||
|
||||
function shouldPreloadRouteSelectedDownload() {
|
||||
return (
|
||||
props.useRouteHash &&
|
||||
!showOptions.value.projectId &&
|
||||
!!getStringQueryValue(route.query.version) &&
|
||||
!!getStringQueryValue(route.query.loader)
|
||||
)
|
||||
}
|
||||
|
||||
function clearCloseStateResetTimeout() {
|
||||
if (!closeStateResetTimeout) return
|
||||
clearTimeout(closeStateResetTimeout)
|
||||
@@ -684,10 +688,133 @@ async function loadProjectForModal(forceRefetch: boolean) {
|
||||
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() {
|
||||
projectDownloadSelection.value = getDefaultProjectDownloadSelection()
|
||||
dependencyDownloadFiles.value = []
|
||||
dependencyDownloadFilesLoaded.value = false
|
||||
downloadProjectResetKey.value += 1
|
||||
}
|
||||
|
||||
@@ -695,6 +822,7 @@ function openFromHash() {
|
||||
if (
|
||||
!props.useRouteHash ||
|
||||
!modal.value ||
|
||||
modalOpening.value ||
|
||||
modalOpen.value ||
|
||||
showProjectId.value ||
|
||||
route.hash !== '#download'
|
||||
@@ -725,12 +853,14 @@ watch(modal, openFromHash)
|
||||
watch(() => route.hash, openFromHash)
|
||||
watch(routeProjectId, () => {
|
||||
projectDownloadSelection.value = getDefaultProjectDownloadSelection()
|
||||
dependencyDownloadFiles.value = []
|
||||
dependencyDownloadFilesLoaded.value = false
|
||||
downloadProjectResetKey.value += 1
|
||||
})
|
||||
|
||||
onUnmounted(clearCloseStateResetTimeout)
|
||||
onUnmounted(() => {
|
||||
unmounted = true
|
||||
modalShowRequestId += 1
|
||||
clearCloseStateResetTimeout()
|
||||
})
|
||||
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user