mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
584 lines
18 KiB
TypeScript
584 lines
18 KiB
TypeScript
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
|
|
metadataLabel?: string
|
|
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(() =>
|
|
hasSkippedDuplicateDependency(dependencyResolution.value) ||
|
|
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,
|
|
metadataLabel: fileTypeLabel(file.file_type),
|
|
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,
|
|
metadataLabel: version?.version_number ?? formatMessage(messages.anyCompatibleDependency),
|
|
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 hasSkippedDuplicateDependency(resolution?: Labrinth.Content.v3.ResolveContentPlan) {
|
|
return (resolution?.skipped || []).some((dependency) => dependency.reason === 'duplicate_project')
|
|
}
|
|
|
|
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({
|
|
anyCompatibleDependency: {
|
|
id: 'project.download.dependency-any-compatible',
|
|
defaultMessage: 'Any compatible',
|
|
},
|
|
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',
|
|
},
|
|
})
|