mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 17:44:50 +00:00
feat: instances v2 (#6431)
* feat: base of instances v2 * feat: use old profiles with compat layer * prototype: instances v2 * fix: install_from using profile * fix: skins migration fix * fix: frontend still using profile path * fix: add update proj multiselect guard * fix: cargo fmt * fix: content missing fields * feat: break up app-lib/api/instance.rs * fix: check_content_updates mismatch * fix: updater modal cleanup w/new structure * feat: better update all handling * fix: remove preview_update_all * fix: feedback on bulk update + lint * fix: rem transitions * fix: change to jsonb * feat: app db backup after update * fix: lint * fix: sqlx prepare + use sqlx macros * fix: lint * fix: bugs * feat: defuck the installing process up * fix: bug of hell * fix: shear * fix: fmt * fix: install progress spacing + change mc/content/overrides to bytes * fix: lint * fix: prepr * fix: navtabs anim not working in app * fix: worlds.vue improvements + browse page fixes * feat: optimise queries + adapter fns * fix: lint * fix: lint * feat: shared modrinth-content-management crate (#6469) * feat: disable warnings setting * feat: add instances shortcuts (#6329) * Add modrinth://launch deep link to start a profile Support external profile launching via modrinth://launch/{profile_path} for integrations such as Stream Deck. * Change route to /launch/profile/{id} for future extensibility * fix: ensure profile path is url decoded * fix: URL-decode profile path from deep link * fix: use urlencoding crate for URL decoding * feat: implement app instance shortcuts * feat: change windows shortcut creation to use windows api instead * feat: implement creating a shortcut launching world/server * format * fmt * fix multiline inline tables * pnpm prepr * feat: move create shortcut to last item * refactor: split up shortcuts.rs for individual platforms * refactor: turn profile launch url into url type * use string literal and add safety comment * pt2 * refactor: rename anything that's profile into instance * update mac shortcut --------- Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com> --------- Co-authored-by: Truman Gao <106889354+tdgao@users.noreply.github.com> Co-authored-by: DJCheesusReal <134006619+DJCheesusReal@users.noreply.github.com>
This commit is contained in:
co-authored by
DJCheesusReal
Truman Gao
parent
ef4044534f
commit
734720e11e
@@ -1,207 +0,0 @@
|
||||
// TODO: migrate to content-install.ts DI
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { get_project, get_version, get_version_many } from '@/helpers/cache.js'
|
||||
import { add_project_from_version, check_installed } from '@/helpers/profile.js'
|
||||
import {
|
||||
add_server_to_profile,
|
||||
get_profile_worlds,
|
||||
resolveManagedServerWorld,
|
||||
} from '@/helpers/worlds.ts'
|
||||
|
||||
export const findPreferredVersion = (versions, project, instance) => {
|
||||
// When `project` is passed in from this stack trace:
|
||||
// - `installVersionDependencies`
|
||||
// - `install.js/install` - `installVersionDependencies` call
|
||||
//
|
||||
// ..then `project` is actually a `Dependency` struct of a cached `Version`.
|
||||
// `Dependency` does not have a `project_type` field,
|
||||
// so we default it to `mod`.
|
||||
//
|
||||
// If we don't default here, then this `.find` will ignore version/instance
|
||||
// loader mismatches, and you'll end up e.g. installing NeoForge mods for a
|
||||
// Fabric instance.
|
||||
const projectType = project.project_type ?? 'mod'
|
||||
|
||||
// If we can find a version using strictly the instance loader then prefer that
|
||||
let version = versions.find(
|
||||
(v) =>
|
||||
v.game_versions.includes(instance.game_version) &&
|
||||
(projectType === 'mod' ? v.loaders.includes(instance.loader) : true),
|
||||
)
|
||||
|
||||
if (!version) {
|
||||
// Otherwise use first compatible version (in addition to versions with the instance loader this includes datapacks)
|
||||
version = versions.find((v) => isVersionCompatible(v, project, instance))
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
export const isVersionCompatible = (version, project, instance) => {
|
||||
return (
|
||||
version.game_versions.includes(instance.game_version) &&
|
||||
(project.project_type === 'mod'
|
||||
? version.loaders.includes(instance.loader) || version.loaders.includes('datapack')
|
||||
: true)
|
||||
)
|
||||
}
|
||||
|
||||
export const installVersionDependencies = async (profile, version, reason, onDepInstalling) => {
|
||||
const projectNames = new Map()
|
||||
const storeProjectName = (p) => {
|
||||
if (p?.id && p.title) projectNames.set(p.id, p.title)
|
||||
}
|
||||
|
||||
const visitedVersions = new Set()
|
||||
const announcedProjects = new Set()
|
||||
const queuedVersionIds = new Set()
|
||||
const queuedProjectVersions = new Map()
|
||||
const queuedInstalls = []
|
||||
const installedProjectCache = new Map()
|
||||
|
||||
const isProjectInstalled = async (projectId) => {
|
||||
if (!projectId) return false
|
||||
if (installedProjectCache.has(projectId)) {
|
||||
return installedProjectCache.get(projectId)
|
||||
}
|
||||
const installed = await check_installed(profile.path, projectId)
|
||||
installedProjectCache.set(projectId, installed)
|
||||
return installed
|
||||
}
|
||||
|
||||
const queueInstall = async (projectId, resolvedVersion, dependentOn) => {
|
||||
if (!resolvedVersion?.id) return false
|
||||
|
||||
const versionId = resolvedVersion.id
|
||||
const resolvedProjectId = projectId ?? resolvedVersion.project_id ?? null
|
||||
|
||||
if (resolvedProjectId) {
|
||||
if (await isProjectInstalled(resolvedProjectId)) return false
|
||||
|
||||
const existingVersionId = queuedProjectVersions.get(resolvedProjectId)
|
||||
if (existingVersionId && existingVersionId !== versionId) return false
|
||||
if (existingVersionId === versionId) return false
|
||||
}
|
||||
|
||||
if (queuedVersionIds.has(versionId)) return false
|
||||
|
||||
queuedVersionIds.add(versionId)
|
||||
if (resolvedProjectId) {
|
||||
queuedProjectVersions.set(resolvedProjectId, versionId)
|
||||
}
|
||||
queuedInstalls.push({
|
||||
versionId,
|
||||
projectId: resolvedProjectId,
|
||||
dependentOnVersionId: dependentOn?.id,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
const announceDependency = async (projectId, resolvedVersion) => {
|
||||
if (!onDepInstalling || !projectId) return
|
||||
if (announcedProjects.has(projectId)) return
|
||||
|
||||
const depProject = await get_project(projectId, 'bypass').catch(() => null)
|
||||
if (!depProject) return
|
||||
|
||||
storeProjectName(depProject)
|
||||
onDepInstalling(depProject, resolvedVersion ?? undefined)
|
||||
announcedProjects.add(projectId)
|
||||
}
|
||||
|
||||
const resolveDependency = async (dep) => {
|
||||
let depVersion = null
|
||||
let depProjectId = dep.project_id ?? null
|
||||
|
||||
if (dep.version_id) {
|
||||
depVersion = await get_version(dep.version_id, 'bypass').catch(() => null)
|
||||
if (!depVersion) return null
|
||||
|
||||
depProjectId = depProjectId ?? depVersion.project_id ?? null
|
||||
if (depProjectId && !projectNames.has(depProjectId)) {
|
||||
const p = await get_project(depProjectId, 'bypass').catch(() => null)
|
||||
storeProjectName(p)
|
||||
}
|
||||
} else if (dep.project_id) {
|
||||
const depProject = await get_project(dep.project_id, 'bypass').catch(() => null)
|
||||
if (!depProject) return null
|
||||
|
||||
storeProjectName(depProject)
|
||||
|
||||
const depVersions = await get_version_many(depProject.versions, 'bypass').catch(() => [])
|
||||
depVersion = findPreferredVersion(
|
||||
depVersions.sort((a, b) => dayjs(b.date_published) - dayjs(a.date_published)),
|
||||
dep,
|
||||
profile,
|
||||
)
|
||||
if (!depVersion) return null
|
||||
|
||||
depProjectId = dep.project_id
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
return { depVersion, depProjectId }
|
||||
}
|
||||
|
||||
const collectDependenciesForVersion = async (inputVersion) => {
|
||||
if (!inputVersion?.id || visitedVersions.has(inputVersion.id)) return
|
||||
visitedVersions.add(inputVersion.id)
|
||||
|
||||
if (inputVersion.project_id && !projectNames.has(inputVersion.project_id)) {
|
||||
const p = await get_project(inputVersion.project_id, 'bypass').catch(() => null)
|
||||
storeProjectName(p)
|
||||
}
|
||||
|
||||
for (const dep of inputVersion.dependencies ?? []) {
|
||||
if (dep.dependency_type !== 'required') continue
|
||||
if (dep.project_id === 'P7dR8mSH' && profile.loader === 'quilt') continue
|
||||
|
||||
const resolved = await resolveDependency(dep, inputVersion)
|
||||
if (!resolved) continue
|
||||
|
||||
const { depVersion, depProjectId } = resolved
|
||||
const queued = await queueInstall(depProjectId, depVersion, inputVersion)
|
||||
if (queued && depProjectId) {
|
||||
await announceDependency(depProjectId, depVersion)
|
||||
}
|
||||
|
||||
await collectDependenciesForVersion(depVersion)
|
||||
}
|
||||
}
|
||||
|
||||
await collectDependenciesForVersion(version)
|
||||
|
||||
if (queuedInstalls.length === 0) return
|
||||
|
||||
const batchSize = 8
|
||||
for (let i = 0; i < queuedInstalls.length; i += batchSize) {
|
||||
const batch = queuedInstalls.slice(i, i + batchSize)
|
||||
await Promise.all(
|
||||
batch.map(async ({ versionId, dependentOnVersionId }) => {
|
||||
await add_project_from_version(profile.path, versionId, reason, dependentOnVersionId)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const getServerAddress = (javaServer) => {
|
||||
if (!javaServer) return null
|
||||
const { address } = javaServer
|
||||
return address
|
||||
}
|
||||
|
||||
export const ensureManagedServerWorldExists = async (profilePath, serverName, serverAddress) => {
|
||||
if (!profilePath || !serverAddress) return
|
||||
try {
|
||||
const worlds = await get_profile_worlds(profilePath)
|
||||
const managedWorld = resolveManagedServerWorld(worlds, serverName, serverAddress)
|
||||
if (!managedWorld) {
|
||||
await add_server_to_profile(profilePath, serverName, serverAddress, 'prompt')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to ensure managed server world exists:', err)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export const DEFAULT_FEATURE_FLAGS = {
|
||||
server_project_qa: false,
|
||||
server_ram_as_bytes_always_on: false,
|
||||
always_show_app_controls: false,
|
||||
skip_non_essential_warnings: false,
|
||||
skip_unknown_pack_warning: false,
|
||||
pride_fundraiser: true,
|
||||
i18n_debug: false,
|
||||
|
||||
Reference in New Issue
Block a user