feat: content tab rewrite for worlds (#5136)
* feat: base content card component * fix: tooltips + colors * feat: fix orgs * feat: base content tab internals rewrite * feat: fix invalidmodal * feat: add ContentModpackCard * fix: extract types * draft: layout * feat: unlink modal * feat: impl content tab * fix: lint * fix: toggling * temp: disable updating stuff * feat: selection v-model * feat: bulk selection * feat: mods tab rough draft * feat: use fuse.js * feat: add project combobox * clean up project combobox * feat: start install to play modal * fix: events * feat: use v-on * feat: bulk actions + fix floating action bar width * feat: figma alignments * feat: migrate toggle to tailwind * fix: row borders * feat: disabled state * feat: virtual list impl for card table based on window scroll * fix: lint * feat: virtualization + smaller contentcard items * feat: use ContentCardTable + ContentCardItems * feat: fix gap + border issues on last elm * feat: cleanup + use proper searching * fix: use TeleportOverflowMenu * fix: fallback to svg if src is invalid on avatar component * fix: storybook * feat: start on updater modal * feat: finish content updater modal * feat: i18n pass * feat: impl modal * feat(app): backend changes for content tab refactor (#5237) * feat: include_changelog=false for updater modal * fix: hash overrides * feat: update checking for modpack * feat: qa * feat: modpack content modal * fix: padding in table to match modals + tightness * fix: lint * feat: delete modal * feat: fix toggle bugs * fix: prepr * fix: duplicate messages * qa: full width search * qa: use bg-surface-1.5 * qa: animation for filter pills * qa: standardize hover colors * fix: border-[1px] is border * qa: mass de-select actually mass selecting * qa: match figma designs for floating action bar * qa: modal fixes * q: modal fixes x2 * fix: table border * qa: confirm modals * qa: modal alignment * qa: re-add stuck heading + dedupe logic * qa: dedupe virtual scrolling + remove dead components * qa: responsiveness for content table + link fixes * qa: version column link, tooltips + lint fixes * qa: instance busy protections * fix: installation freeze bug * chore: remove old mods page * refactor: deduplicate layout * chore: delete old content page(s) * qa * qa * qa * feat: sort btn - to iterate * fix: ml * feat: date added * fix: lint * fix: formatting.ts removal * feat: get_dependencies_as_content_items * qa: final QA changes * refactor: deduplicate + polish content.rs * feat: hook up content.vue with v1 * feat: hide v1 content api behind frontend feature flag * fix: query keys + copy on empty state * chore: i18n pass * feat: reimpl unlink + upload endpoint * feat: use bulk endpoints v1 * fix: lint * fix: flags * fix: responsiveness via container queries * fix: lint * qa: 1 * qa: fixes * qa: fix ssr issues with browse content * qa: header page divider * qa: modals * fix: prepr * fix: issues * fix: lint * fix: toggle v1 ff * qa: 5 * qa: delete modal copy * feat: creation flow modals (#5383) * refactor: delete content v0 usages + impl * feat: qa + fixes * feat: installing banner using state event * feat: fix modpack card bugs + filtering issues * refactor: delete backups v0 api module * feat: v1 servers GET endpoint * fix: backups * feat: swap to kyros upload v1 addon * fix: use tanstack for loader.vue * feat: finish install from discovery modal * qa: bug fixes * feat: set up installation settings * fix: lint * fix: typos * fix: bugs * fix: disable inline content * feat: content tab improvements — upload UX, installation settings, and client-only indicators Upload cancellation and navigation guard: - Add ConfirmLeaveModal that prompts when navigating away during upload - Cancel in-flight XHR uploads when user confirms leaving the page - Add beforeunload handler to warn on browser/tab close during upload - Track uploadedBytes/totalBytes in UploadState for progress display - Replace Collapsible with Transition for upload progress admonition - Show byte progress and percentage in upload banner - Clamp upload progress to prevent exceeding 100% Installation settings (server.properties): - Add KnownPropertiesFields and PropertiesFields types to Archon types - Add buildProperties() to creation flow context to collect gamemode, difficulty, seed, world type, structures, and generator settings - Pass properties through installContent on onboarding, discovery, and ServerSetupModal flows Server setup and discovery flow improvements: - Migrate ServerSetupModal from servers_v0.reinstall to content_v1.installContent - Replace loaderApiNames lookup with toApiLoader() helper - Remove eraseDataOnInstall toggle — always use soft_override: false - Simplify modpack install on discovery page to use first available version and route through creation flow modal for both onboarding and non-onboarding - Differentiate post-install navigation: content page for onboarding, loader options for existing servers Modpack update flow: - Replace updateModpack() call with installContent() using soft_override: true to support version selection in the content updater modal Client-only mod indicators: - Add environment field to AddonVersion (reuses Labrinth.Projects.v3.Environment) - Add environment to ContentItem and isClientOnly to ContentCardTableItem - Show orange TriangleAlertIcon with tooltip on client-only mods in content table - Add "Client-only" filter pill to content filtering (controlled via showClientOnlyFilter on ContentManagerContext) - Apply client-only indicators in both ContentPageLayout and ModpackContentModal Misc: - Add CLAUDE.md note about using prepr commands for lint checks - Export ConfirmLeaveModal from instances barrel * fix: piping * fix: switch content disable for linked server instances * feat: client only filter * fix: prepr * feat: hasUpdate shape update * feat: bulk update endpoint impl for content in panel * feat: websocket state impl again with new phases * fix: ws * fix: use timeout fn for sync admon + fix content card layout scroll for browsers with overflow anchor bug * fix: qa bugs * fix: lint, a11y and i18n * refactor: set up layouts folder properly * fix: linked data cache stuff + lint * feat: move installationsettings to shared layout * fix: lint * fix: issues * feat: temp fuck staging up * fix: lockfile * fix: data sync issues on loader.vue * fix: lint * Hide shader configuration files from content list (#5499) * feat: workaround search problem + split out reset * fix: qa * fix: changelog not showing on first open * fix: qa + optimistic updating improvements * fix: prepr+lint * fix: qa * feat: qa * fix: lint * fix: lint * fix: build * fix: build * fix: type errors * fix: fade and JAVA_HOME passthrough * feat: qa * feat: impl diff shit * fix: qa * fix: app qa * feat: update diff modal * fix: endpoint * fix: qa * fix: qa * fix: use bulk in modpack modal * feat: abort signal impl + fix issues * fix: diff modal trunc * feat: qa * fix: qa * feat: tooltip content tab * fix: prepr * fix: dismiss on settings btn * feat: qa * feat: dont clear handlers on disconnect * fix: lint * fix: wrangler + introduce staging-archon env file --------- Signed-off-by: Calum H. <calum@modrinth.com> Co-authored-by: tdgao <mr.trumgao@gmail.com> Co-authored-by: Artyom Ezri <61311568+Artezon@users.noreply.github.com>
This commit is contained in:
515
apps/app-frontend/src/providers/content-install.ts
Normal file
515
apps/app-frontend/src/providers/content-install.ts
Normal file
@@ -0,0 +1,515 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import type { ContentInstallInstance, ContentItem } from '@modrinth/ui'
|
||||
import { createContext } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
import { nextTick, type Ref, ref } from 'vue'
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project, get_project_v3_many, get_version_many } from '@/helpers/cache.js'
|
||||
import { create_profile_and_install as packInstall } from '@/helpers/pack'
|
||||
import {
|
||||
add_project_from_version,
|
||||
check_installed_batch,
|
||||
create,
|
||||
get,
|
||||
get_projects,
|
||||
list,
|
||||
remove_project,
|
||||
} from '@/helpers/profile.js'
|
||||
import { get_game_versions } from '@/helpers/tags'
|
||||
import {
|
||||
findPreferredVersion,
|
||||
installVersionDependencies,
|
||||
isVersionCompatible,
|
||||
} from '@/store/install.js'
|
||||
|
||||
interface ModalRef {
|
||||
show: () => void
|
||||
hide: () => void
|
||||
}
|
||||
|
||||
interface InstallConfirmModalRef {
|
||||
show: (
|
||||
project: Labrinth.Projects.v2.Project,
|
||||
version: string,
|
||||
callback: (versionId?: string) => void,
|
||||
createInstanceCallback: (profile: string) => void,
|
||||
) => void
|
||||
}
|
||||
|
||||
interface IncompatibilityWarningModalRef {
|
||||
show: (
|
||||
instance: GameInstance,
|
||||
project: Labrinth.Projects.v2.Project,
|
||||
versions: Labrinth.Versions.v2.Version[],
|
||||
version: Labrinth.Versions.v2.Version,
|
||||
callback: (versionId?: string) => void,
|
||||
) => void
|
||||
}
|
||||
|
||||
const LOADER_ORDER = ['vanilla', 'fabric', 'quilt', 'neoforge', 'forge']
|
||||
const SUPPORTED_LOADERS: Set<string> = new Set(['vanilla', 'forge', 'fabric', 'quilt', 'neoforge'])
|
||||
const VANILLA_COMPATIBLE_LOADERS: Set<string> = new Set(['minecraft', 'datapack'])
|
||||
|
||||
function sortLoaders(loaders: string[]): string[] {
|
||||
return loaders.slice().sort((a, b) => {
|
||||
const aIdx = LOADER_ORDER.indexOf(a)
|
||||
const bIdx = LOADER_ORDER.indexOf(b)
|
||||
if (aIdx === -1 && bIdx === -1) return a.localeCompare(b)
|
||||
if (aIdx === -1) return 1
|
||||
if (bIdx === -1) return -1
|
||||
return aIdx - bIdx
|
||||
})
|
||||
}
|
||||
|
||||
export interface ContentInstallContext {
|
||||
instances: Ref<ContentInstallInstance[]>
|
||||
compatibleLoaders: Ref<string[]>
|
||||
gameVersions: Ref<string[]>
|
||||
loading: Ref<boolean>
|
||||
defaultTab: Ref<'existing' | 'new'>
|
||||
preferredLoader: Ref<string | null>
|
||||
preferredGameVersion: Ref<string | null>
|
||||
releaseGameVersions: Ref<Set<string>>
|
||||
handleInstallToInstance: (instance: ContentInstallInstance) => Promise<void>
|
||||
handleCreateAndInstall: (data: {
|
||||
name: string
|
||||
iconPath: string | null
|
||||
iconPreviewUrl: string | null
|
||||
loader: string
|
||||
gameVersion: string
|
||||
}) => Promise<void>
|
||||
handleNavigate: (instance: ContentInstallInstance) => void
|
||||
handleCancel: () => void
|
||||
setContentInstallModal: (ref: ModalRef) => void
|
||||
setInstallConfirmModal: (ref: InstallConfirmModalRef) => void
|
||||
setIncompatibilityWarningModal: (ref: IncompatibilityWarningModalRef) => void
|
||||
install: (
|
||||
projectId: string,
|
||||
versionId?: string | null,
|
||||
instancePath?: string | null,
|
||||
source?: string,
|
||||
callback?: (versionId?: string) => void,
|
||||
createInstanceCallback?: (profile: string) => void,
|
||||
hints?: { preferredLoader?: string; preferredGameVersion?: string },
|
||||
) => Promise<void>
|
||||
installingItems: Ref<Map<string, ContentItem[]>>
|
||||
}
|
||||
|
||||
export const [injectContentInstall, provideContentInstall] = createContext<ContentInstallContext>(
|
||||
'root',
|
||||
'contentInstall',
|
||||
)
|
||||
|
||||
export function createContentInstall(opts: {
|
||||
router: Router
|
||||
handleError: (err: unknown) => void
|
||||
}): ContentInstallContext {
|
||||
const instances = ref<ContentInstallInstance[]>([])
|
||||
const compatibleLoaders = ref<string[]>([])
|
||||
const gameVersions = ref<string[]>([])
|
||||
const loading = ref(false)
|
||||
const defaultTab = ref<'existing' | 'new'>('existing')
|
||||
const preferredLoader = ref<string | null>(null)
|
||||
const preferredGameVersion = ref<string | null>(null)
|
||||
const releaseGameVersions = ref<Set<string>>(new Set())
|
||||
|
||||
const installingItems = ref<Map<string, ContentItem[]>>(new Map())
|
||||
|
||||
function addInstallingItem(
|
||||
instancePath: string,
|
||||
project: {
|
||||
id: string
|
||||
slug?: string | null
|
||||
title: string
|
||||
icon_url?: string | null
|
||||
project_type?: string
|
||||
},
|
||||
) {
|
||||
const placeholder: ContentItem = {
|
||||
file_name: `__installing_${project.id}`,
|
||||
project: {
|
||||
id: project.id,
|
||||
slug: project.slug ?? null,
|
||||
title: project.title,
|
||||
icon_url: project.icon_url ?? null,
|
||||
},
|
||||
project_type: project.project_type ?? 'mod',
|
||||
has_update: false,
|
||||
update_version_id: null,
|
||||
enabled: true,
|
||||
installing: true,
|
||||
}
|
||||
const next = new Map(installingItems.value)
|
||||
const items = next.get(instancePath) ?? []
|
||||
if (items.some((i) => i.file_name === placeholder.file_name)) return
|
||||
next.set(instancePath, [...items, placeholder])
|
||||
installingItems.value = next
|
||||
}
|
||||
|
||||
function removeInstallingItems(instancePath: string, projectIds: string[]) {
|
||||
const next = new Map(installingItems.value)
|
||||
const items = next.get(instancePath)
|
||||
if (items) {
|
||||
const idsToRemove = new Set(projectIds.map((id) => `__installing_${id}`))
|
||||
const filtered = items.filter((i) => !idsToRemove.has(i.file_name))
|
||||
if (filtered.length > 0) {
|
||||
next.set(instancePath, filtered)
|
||||
} else {
|
||||
next.delete(instancePath)
|
||||
}
|
||||
installingItems.value = next
|
||||
}
|
||||
}
|
||||
|
||||
let modalRef: ModalRef | null = null
|
||||
let installConfirmModalRef: InstallConfirmModalRef | null = null
|
||||
let incompatibilityWarningModalRef: IncompatibilityWarningModalRef | null = null
|
||||
let currentProject: Labrinth.Projects.v2.Project | null = null
|
||||
let currentVersions: Labrinth.Versions.v2.Version[] = []
|
||||
let currentCallback: (versionId?: string) => void = () => {}
|
||||
let profileMap: Record<string, GameInstance> = {}
|
||||
|
||||
async function showModInstallModal(
|
||||
project: Labrinth.Projects.v2.Project,
|
||||
versions: Labrinth.Versions.v2.Version[],
|
||||
onInstall: (versionId?: string) => void,
|
||||
hints?: { preferredLoader?: string; preferredGameVersion?: string },
|
||||
) {
|
||||
currentProject = project
|
||||
currentVersions = versions
|
||||
currentCallback = onInstall
|
||||
|
||||
instances.value = []
|
||||
defaultTab.value = 'existing'
|
||||
|
||||
const loaderSet = new Set<string>()
|
||||
const gameVersionSet = new Set<string>()
|
||||
for (const v of versions) {
|
||||
for (const l of v.loaders) loaderSet.add(l)
|
||||
for (const gv of v.game_versions) gameVersionSet.add(gv)
|
||||
}
|
||||
const mappedLoaders = new Set<string>()
|
||||
for (const l of loaderSet) {
|
||||
if (SUPPORTED_LOADERS.has(l)) mappedLoaders.add(l)
|
||||
else if (VANILLA_COMPATIBLE_LOADERS.has(l)) mappedLoaders.add('vanilla')
|
||||
}
|
||||
compatibleLoaders.value = sortLoaders([...mappedLoaders])
|
||||
|
||||
try {
|
||||
const allGameVersions = await get_game_versions()
|
||||
const releases = new Set<string>()
|
||||
const ordered: string[] = []
|
||||
for (const gv of allGameVersions) {
|
||||
if (gameVersionSet.has(gv.version)) {
|
||||
ordered.push(gv.version)
|
||||
if (gv.version_type === 'release') {
|
||||
releases.add(gv.version)
|
||||
}
|
||||
}
|
||||
}
|
||||
gameVersions.value = ordered
|
||||
releaseGameVersions.value = releases
|
||||
} catch {
|
||||
gameVersions.value = [...gameVersionSet]
|
||||
releaseGameVersions.value = new Set(gameVersionSet)
|
||||
}
|
||||
|
||||
preferredLoader.value =
|
||||
hints?.preferredLoader && loaderSet.has(hints.preferredLoader) ? hints.preferredLoader : null
|
||||
preferredGameVersion.value =
|
||||
hints?.preferredGameVersion && gameVersionSet.has(hints.preferredGameVersion)
|
||||
? hints.preferredGameVersion
|
||||
: null
|
||||
|
||||
try {
|
||||
let profiles = await list()
|
||||
|
||||
const linkedProjectIds = profiles
|
||||
.filter((p) => p.linked_data?.project_id)
|
||||
.map((p) => p.linked_data!.project_id)
|
||||
if (linkedProjectIds.length > 0) {
|
||||
const linkedProjects = await get_project_v3_many(linkedProjectIds, 'must_revalidate').catch(
|
||||
() => [],
|
||||
)
|
||||
const serverProjectIds = new Set(
|
||||
linkedProjects
|
||||
.filter((p: { id: string; minecraft_server?: unknown }) => p?.minecraft_server != null)
|
||||
.map((p: { id: string }) => p.id),
|
||||
)
|
||||
profiles = profiles.filter(
|
||||
(p) => !p.linked_data?.project_id || !serverProjectIds.has(p.linked_data.project_id),
|
||||
)
|
||||
}
|
||||
|
||||
const newProfileMap: Record<string, GameInstance> = {}
|
||||
const installedMap = await check_installed_batch(project.id)
|
||||
|
||||
const newInstances: ContentInstallInstance[] = profiles.map((profile) => {
|
||||
newProfileMap[profile.path] = profile
|
||||
return {
|
||||
id: profile.path,
|
||||
name: profile.name,
|
||||
iconUrl: profile.icon_path ? convertFileSrc(profile.icon_path) : null,
|
||||
installed: installedMap[profile.path] ?? false,
|
||||
compatible: versions.some((v) => isVersionCompatible(v, project, profile)),
|
||||
installing: false,
|
||||
}
|
||||
})
|
||||
|
||||
profileMap = newProfileMap
|
||||
instances.value = newInstances
|
||||
|
||||
if (!newInstances.some((i) => i.compatible && !i.installed)) {
|
||||
defaultTab.value = 'new'
|
||||
}
|
||||
} catch (err) {
|
||||
opts.handleError(err)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
modalRef?.show()
|
||||
trackEvent('ProjectInstallStart', { source: 'ProjectInstallModal' })
|
||||
}
|
||||
|
||||
async function handleInstallToInstance(instance: ContentInstallInstance) {
|
||||
const profile = profileMap[instance.id]
|
||||
const storeInstance = instances.value.find((i) => i.id === instance.id)
|
||||
if (storeInstance) storeInstance.installing = true
|
||||
|
||||
const version = findPreferredVersion(currentVersions, currentProject, profile)
|
||||
if (!version) {
|
||||
if (storeInstance) storeInstance.installing = false
|
||||
opts.handleError('No compatible version found')
|
||||
return
|
||||
}
|
||||
|
||||
const installedProjectIds: string[] = []
|
||||
if (currentProject) {
|
||||
addInstallingItem(instance.id, currentProject)
|
||||
installedProjectIds.push(currentProject.id)
|
||||
}
|
||||
|
||||
try {
|
||||
await add_project_from_version(instance.id, version.id)
|
||||
await installVersionDependencies(
|
||||
profile,
|
||||
version,
|
||||
(depProject: Labrinth.Projects.v2.Project) => {
|
||||
addInstallingItem(instance.id, depProject)
|
||||
installedProjectIds.push(depProject.id)
|
||||
},
|
||||
)
|
||||
if (storeInstance) {
|
||||
storeInstance.installed = true
|
||||
storeInstance.installing = false
|
||||
}
|
||||
trackEvent('ProjectInstall', {
|
||||
loader: profile.loader,
|
||||
game_version: profile.game_version,
|
||||
id: currentProject.id,
|
||||
version_id: version.id,
|
||||
project_type: currentProject.project_type,
|
||||
title: currentProject.title,
|
||||
source: 'ProjectInstallModal',
|
||||
})
|
||||
currentCallback(version.id)
|
||||
} catch (err) {
|
||||
if (storeInstance) storeInstance.installing = false
|
||||
opts.handleError(err)
|
||||
} finally {
|
||||
removeInstallingItems(instance.id, installedProjectIds)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateAndInstall(data: {
|
||||
name: string
|
||||
iconPath: string | null
|
||||
iconPreviewUrl: string | null
|
||||
loader: string
|
||||
gameVersion: string
|
||||
}) {
|
||||
const loaderCandidates =
|
||||
data.loader === 'vanilla' ? ['vanilla', 'datapack', 'minecraft'] : [data.loader]
|
||||
const version =
|
||||
currentVersions.find(
|
||||
(v) =>
|
||||
v.game_versions.includes(data.gameVersion) &&
|
||||
loaderCandidates.some((l) => v.loaders.includes(l)),
|
||||
) ?? currentVersions[0]
|
||||
|
||||
try {
|
||||
const id = await create(
|
||||
data.name,
|
||||
data.gameVersion,
|
||||
data.loader as InstanceLoader,
|
||||
'latest',
|
||||
data.iconPath,
|
||||
false,
|
||||
)
|
||||
if (!id) return
|
||||
|
||||
await add_project_from_version(id, version.id)
|
||||
await opts.router.push(`/instance/${encodeURIComponent(id)}/`)
|
||||
|
||||
const instance = await get(id)
|
||||
await installVersionDependencies(instance, version)
|
||||
|
||||
trackEvent('InstanceCreate', {
|
||||
source: 'ProjectInstallModal',
|
||||
})
|
||||
trackEvent('ProjectInstall', {
|
||||
loader: data.loader,
|
||||
game_version: data.gameVersion,
|
||||
id: currentProject.id,
|
||||
version_id: version.id,
|
||||
project_type: currentProject.project_type,
|
||||
title: currentProject.title,
|
||||
source: 'ProjectInstallModal',
|
||||
})
|
||||
|
||||
currentCallback(version.id)
|
||||
modalRef?.hide()
|
||||
} catch (err) {
|
||||
opts.handleError(err)
|
||||
}
|
||||
}
|
||||
|
||||
function handleNavigate(instance: ContentInstallInstance) {
|
||||
modalRef?.hide()
|
||||
opts.router.push(`/instance/${encodeURIComponent(instance.id)}/`)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
currentCallback?.()
|
||||
}
|
||||
|
||||
async function install(
|
||||
projectId: string,
|
||||
versionId?: string | null,
|
||||
instancePath?: string | null,
|
||||
source: string = 'unknown',
|
||||
callback: (versionId?: string) => void = () => {},
|
||||
createInstanceCallback: (profile: string) => void = () => {},
|
||||
hints?: { preferredLoader?: string; preferredGameVersion?: string },
|
||||
) {
|
||||
const project: Labrinth.Projects.v2.Project = await get_project(projectId, 'must_revalidate')
|
||||
|
||||
if (project.project_type === 'modpack') {
|
||||
const version = versionId ?? project.versions[project.versions.length - 1]
|
||||
const packs = await list()
|
||||
|
||||
if (
|
||||
packs.length === 0 ||
|
||||
!packs.find((pack) => pack.linked_data?.project_id === project.id)
|
||||
) {
|
||||
await packInstall(
|
||||
project.id,
|
||||
version,
|
||||
project.title,
|
||||
project.icon_url,
|
||||
createInstanceCallback,
|
||||
)
|
||||
trackEvent('PackInstall', {
|
||||
id: project.id,
|
||||
version_id: version,
|
||||
title: project.title,
|
||||
source,
|
||||
})
|
||||
callback(version)
|
||||
} else {
|
||||
installConfirmModalRef?.show(project, version, callback, createInstanceCallback)
|
||||
}
|
||||
} else if (instancePath) {
|
||||
const [instanceOrNull, instanceProjects, versions] = await Promise.all([
|
||||
get(instancePath),
|
||||
get_projects(instancePath),
|
||||
get_version_many(project.versions, 'must_revalidate') as Promise<
|
||||
Labrinth.Versions.v2.Version[]
|
||||
>,
|
||||
])
|
||||
if (!instanceOrNull) return
|
||||
|
||||
const instance = instanceOrNull
|
||||
const projectVersions = versions.sort(
|
||||
(a, b) => dayjs(b.date_published).valueOf() - dayjs(a.date_published).valueOf(),
|
||||
)
|
||||
|
||||
let version = versionId
|
||||
? projectVersions.find((v) => v.id === versionId)
|
||||
: findPreferredVersion(projectVersions, project, instance)
|
||||
if (!version) version = projectVersions[0]
|
||||
|
||||
if (isVersionCompatible(version, project, instance)) {
|
||||
for (const [path, file] of Object.entries(instanceProjects)) {
|
||||
if (file.metadata?.project_id === project.id) {
|
||||
await remove_project(instance.path, path)
|
||||
}
|
||||
}
|
||||
|
||||
const installedProjectIds: string[] = [project.id]
|
||||
addInstallingItem(instancePath, project)
|
||||
try {
|
||||
await add_project_from_version(instance.path, version.id)
|
||||
await installVersionDependencies(
|
||||
instance,
|
||||
version,
|
||||
(depProject: Labrinth.Projects.v2.Project) => {
|
||||
addInstallingItem(instancePath, depProject)
|
||||
installedProjectIds.push(depProject.id)
|
||||
},
|
||||
)
|
||||
|
||||
trackEvent('ProjectInstall', {
|
||||
loader: instance.loader,
|
||||
game_version: instance.game_version,
|
||||
id: project.id,
|
||||
project_type: project.project_type,
|
||||
version_id: version.id,
|
||||
title: project.title,
|
||||
source,
|
||||
})
|
||||
callback(version.id)
|
||||
} finally {
|
||||
removeInstallingItems(instancePath, installedProjectIds)
|
||||
}
|
||||
} else {
|
||||
incompatibilityWarningModalRef?.show(instance, project, projectVersions, version, callback)
|
||||
}
|
||||
} else {
|
||||
let versions = (
|
||||
(await get_version_many(project.versions)) as Labrinth.Versions.v2.Version[]
|
||||
).sort((a, b) => dayjs(b.date_published).valueOf() - dayjs(a.date_published).valueOf())
|
||||
if (versionId) versions = versions.filter((v) => v.id === versionId)
|
||||
await showModInstallModal(project, versions, callback, hints)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
instances,
|
||||
compatibleLoaders,
|
||||
gameVersions,
|
||||
loading,
|
||||
defaultTab,
|
||||
preferredLoader,
|
||||
preferredGameVersion,
|
||||
releaseGameVersions,
|
||||
handleInstallToInstance,
|
||||
handleCreateAndInstall,
|
||||
handleNavigate,
|
||||
handleCancel,
|
||||
setContentInstallModal(ref: ModalRef) {
|
||||
modalRef = ref
|
||||
},
|
||||
setInstallConfirmModal(ref: InstallConfirmModalRef) {
|
||||
installConfirmModalRef = ref
|
||||
},
|
||||
setIncompatibilityWarningModal(ref: IncompatibilityWarningModalRef) {
|
||||
incompatibilityWarningModalRef = ref
|
||||
},
|
||||
install,
|
||||
installingItems,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user