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:
Calum H.
2026-06-25 21:19:29 +00:00
committed by GitHub
co-authored by DJCheesusReal Truman Gao
parent ef4044534f
commit 734720e11e
353 changed files with 24745 additions and 9771 deletions
-207
View File
@@ -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)
}
}
+1
View File
@@ -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,