import { invoke } from '@tauri-apps/api/core' import type { InstallJobSnapshot } from './install' export type ContentProvider = 'modrinth' | 'curseforge' export interface CurseForgeCapability { status: 'missing_key' | 'ready' | 'unauthorized' configured: boolean } export interface CurseForgeSearchRequest { classId: number categoryId?: number categoryIds?: number[] searchFilter?: string gameVersion?: string modLoaderType?: number sortField?: number sortOrder?: 'asc' | 'desc' index?: number pageSize?: number } export interface UnifiedSearchHit { provider: 'curseforge' project_id: string slug?: string author: string author_url?: string title: string description: string project_type: string categories: string[] versions: string[] downloads: number icon_url?: string date_created: string date_modified: string latest_version?: string gallery: string[] website_url?: string source_url?: string allow_mod_distribution?: boolean } export interface UnifiedSearchResponse { provider: 'curseforge' hits: UnifiedSearchHit[] offset: number limit: number total_hits: number } export interface CurseForgeFilesRequest { gameVersion?: string modLoaderType?: number gameVersionTypeId?: number index?: number pageSize?: number } export interface CurseForgeProject { id: number name: string slug: string summary: string downloadCount: number mainFileId: number classId?: number dateCreated: string dateModified: string dateReleased: string allowModDistribution?: boolean gamePopularityRank?: number logo?: { thumbnailUrl: string; url: string } authors: Array<{ id: number; name: string; url: string }> categories: Array<{ id: number; name: string; slug: string; iconUrl?: string }> screenshots: Array<{ id: number; title: string; url: string; thumbnailUrl: string }> latestFilesIndexes: Array<{ gameVersion: string fileId: number filename: string releaseType: number gameVersionTypeId?: number modLoader?: number }> links: { websiteUrl?: string wikiUrl?: string issuesUrl?: string sourceUrl?: string } } export interface CurseForgeCategory { id: number gameId: number name: string slug: string url: string iconUrl?: string dateModified: string isClass?: boolean | null classId?: number parentCategoryId?: number displayIndex?: number } export interface CurseForgeFile { id: number modId: number isAvailable: boolean displayName: string fileName: string releaseType: number fileDate: string fileLength: number hashes: Array<{ value: string; algo: number }> fileFingerprint: number downloadCount: number downloadUrl?: string gameVersions: string[] dependencies: Array<{ modId: number; relationType: number }> } export function hasCompatibleCurseForgeFile(files: CurseForgeFile[], gameVersion: string) { return files.some((file) => file.isAvailable && file.gameVersions.includes(gameVersion)) } export function getCurseForgeImageUrl(source?: string | null, width = 256): string | undefined { if (!source) return undefined try { const url = new URL(source) if (url.protocol !== 'https:' || !url.hostname.endsWith('forgecdn.net')) return source const proxy = new URL('https://images.weserv.nl/') proxy.searchParams.set('url', source) proxy.searchParams.set('w', String(width)) proxy.searchParams.set('fit', 'contain') proxy.searchParams.set('output', 'webp') return proxy.toString() } catch { return source } } export interface CurseForgeFilesResponse { files: CurseForgeFile[] pagination: { index: number pageSize: number resultCount: number totalCount: number } } export interface CurseForgeInstallRequest { instanceId: string projectId: number fileId: number projectType: string ownershipKind?: 'pack_managed' | 'user_added' manualOperationKind?: 'pack_install' | 'pack_update' | 'content_install' | 'content_update' gameVersion?: string modLoaderType?: number worldName?: string installDependencies?: boolean excludedDependencyProjectIds?: number[] forceDependencyProjectIds?: number[] dependencyPlanId?: string } export interface CurseForgeWorldInstallRequest { instanceId: string projectId: number fileId: number } export interface CurseForgeInstallResult { installed: Array<{ projectId: number fileId: number relativePath: string dependency: boolean }> manualDownloads: Array<{ projectId: number fileId: number fileName: string ownershipKind: 'pack_managed' | 'user_added' operationKind: 'pack_install' | 'pack_update' | 'content_install' | 'content_update' websiteUrl?: string projectType: string projectSlug: string targetFolder: string hashes: Array<{ value: string; algo: number }> fileLength: number fileFingerprint: number }> failedDownloads: Array<{ projectId: number fileId: number fileName: string reason: string }> optionalDependencies: number[] incompatibleDependencies: number[] skippedDependencies?: Array<{ projectId: number fileId: number | null reason: string }> } export interface CurseForgeInstallPreview { planId: string primary: { projectId: number fileId: number title: string versionNumber: string fileName: string size: number requiredByProjectIds: number[] iconUrl?: string | null } dependencies: Array<{ projectId: number fileId: number title: string versionNumber: string fileName: string size: number requiredByProjectIds: number[] iconUrl?: string | null versionMismatch?: boolean selectionReason?: 'native_strict_match' | 'sha1_verified_modrinth_fallback' required?: boolean }> modrinthFallbacks?: Array<{ projectId: string versionId: string title: string versionNumber: string parentProjectId: number iconUrl?: string | null required?: boolean }> skipped: Array<{ projectId: number fileId: number | null reason: string }> optionalDependencies: number[] incompatibleDependencies: number[] } export interface CurseForgeManualDownloadImport { projectId: number fileId: number relativePath: string } export type CurseForgeManualDownloadImportErrorKind = | 'not_pending' | 'verification_failed' | 'other' export interface CurseForgeManualDownloadScanResult { downloadDirectory?: string | null imported: CurseForgeManualDownloadImport[] errors: Array<{ projectId: number fileId: number message: string }> } export interface CurseForgeModpackInstallResult { content: CurseForgeInstallResult overridesWritten: number minecraftVersion: string loader?: string } export function summarizeCurseForgeInstall(result: CurseForgeInstallResult) { const installed = result.installed?.length ?? 0 const manual = result.manualDownloads?.length ?? 0 const failed = result.failedDownloads?.length ?? 0 const optional = result.optionalDependencies?.length ?? 0 const incompatible = result.incompatibleDependencies?.length ?? 0 return { installed, manual, failed, optional, incompatible } } function getErrorMessage(error: unknown): string | null { if (error instanceof Error) return error.message if (typeof error === 'string') return error if (typeof error !== 'object' || error === null || !('message' in error)) return null return String(error.message) } export function classifyCurseForgeManualDownloadImportError( error: unknown, ): CurseForgeManualDownloadImportErrorKind { const message = getErrorMessage(error) if (message?.includes('The selected CurseForge file is not pending for this instance')) { return 'not_pending' } if (message?.includes('The selected file does not match the required CurseForge file')) { return 'verification_failed' } return 'other' } export function getCurseForgeDownloadFailureDetails(error: unknown): string | null { const message = getErrorMessage(error) if (!message) return null const normalized = message.toLowerCase() const isDownloadFailure = normalized.includes('download failed after') const isCurseForge = normalized.includes('curseforge') || normalized.includes('forgecdn.net') || normalized.includes('forgecdn') return isDownloadFailure && isCurseForge ? message : null } export function getCurseForgeCapability() { return invoke('plugin:curseforge|curseforge_capability') } export function validateCurseForgeCredentials() { return invoke('plugin:curseforge|curseforge_validate_credentials') } export function searchCurseForgeProjects(request: CurseForgeSearchRequest, requestId?: string) { return invoke('plugin:curseforge|curseforge_search_projects', { request, requestId, }) } export function getCurseForgeProject(projectId: number) { return invoke('plugin:curseforge|curseforge_get_project', { projectId }) } export function getCurseForgeProjects(projectIds: number[], cacheBehaviour?: CacheBehaviour) { return invoke('plugin:curseforge|curseforge_get_projects', { projectIds, cacheBehaviour, }) } export function getCurseForgeChangelog(projectId: number, fileId: number) { return invoke('plugin:curseforge|curseforge_get_changelog', { projectId, fileId, }) } export function getCurseForgeDescription(projectId: number) { return invoke('plugin:curseforge|curseforge_get_description', { projectId, }) } export function getCurseForgeFiles(projectId: number, request: CurseForgeFilesRequest) { return invoke('plugin:curseforge|curseforge_get_files', { projectId, request, }) } export function getCurseForgeFile(projectId: number, fileId: number) { return invoke('plugin:curseforge|curseforge_get_file', { projectId, fileId, }) } export function getCurseForgeDownloadUrl(projectId: number, fileId: number) { return invoke('plugin:curseforge|curseforge_get_download_url', { projectId, fileId, }) } export function getCurseForgeCategories(classId?: number) { return invoke('plugin:curseforge|curseforge_get_categories', { classId }) } export function installCurseForgeFile(request: CurseForgeInstallRequest) { return invoke('plugin:curseforge|curseforge_install_file', { request }) } export function previewCurseForgeFile(request: CurseForgeInstallRequest) { return invoke('plugin:curseforge|curseforge_preview_install_file', { request, }) } export function queueCurseForgeFile( request: CurseForgeInstallRequest, display: { title: string; iconUrl?: string | null }, ) { return invoke('plugin:instance|instance_queue_curseforge_content', { request, displayTitle: display.title, displayIcon: display.iconUrl ?? null, }) } export function queueCurseForgeWorld( request: CurseForgeWorldInstallRequest, display: { title: string; iconUrl?: string | null }, ) { return invoke('plugin:instance|instance_queue_curseforge_world', { request, displayTitle: display.title, displayIcon: display.iconUrl ?? null, }) } export function updateCurseForgeFile(instanceId: string, relativePath: string) { return invoke('plugin:curseforge|curseforge_update_installed_file', { instanceId, relativePath, }) } export function switchCurseForgeFileVersion( instanceId: string, relativePath: string, fileId: number, ) { return invoke( 'plugin:curseforge|curseforge_switch_installed_file_version', { instanceId, relativePath, fileId, }, ) } export function recognizeCurseForgeFiles(instanceId: string) { return invoke<{ scanned: number matched: number linked: CurseForgeInstallResult['installed'] unmatchedPaths: string[] }>('plugin:curseforge|curseforge_recognize_instance_files', { instanceId }) } export function importCurseForgeManualDownloads(instanceId: string, scanDirectory?: string | null) { return invoke( 'plugin:curseforge|curseforge_import_manual_downloads', { instanceId, scanDirectory: scanDirectory ?? null }, ) } export function listPendingCurseForgeManualDownloads(instanceId: string) { return invoke( 'plugin:curseforge|curseforge_list_pending_manual_downloads', { instanceId }, ) } export function importPendingCurseForgeManualDownloadFile( instanceId: string, projectId: number, fileId: number, sourcePath: string, ) { return invoke( 'plugin:curseforge|curseforge_import_pending_manual_download_file', { instanceId, projectId, fileId, sourcePath }, ) } export function configureCurseForgeManualDownloadWatcher( enabled: boolean, scanDirectory?: string | null, ) { return invoke('plugin:curseforge|curseforge_configure_manual_download_watcher', { enabled, scanDirectory: scanDirectory ?? null, }) } export function installCurseForgeModpack(request: { instanceId: string projectId: number fileId: number installOptional?: boolean }) { return invoke('plugin:curseforge|curseforge_install_modpack', { request, }) } export function updateManagedCurseForgeModpack(instanceId: string, fileId: number) { return invoke('plugin:curseforge|curseforge_update_managed_modpack', { instanceId, fileId, }) }