mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
feat: content upload session impl (#6992)
* feat: content upload session impl * fix: fixes * fix: prepr
This commit is contained in:
@@ -123,6 +123,7 @@ const {
|
||||
effectiveServerWorldId,
|
||||
serverContextServerData,
|
||||
serverContentProjectIds,
|
||||
queuedServerInstallRootProjectIds,
|
||||
queuedServerInstallProjectIds,
|
||||
queuedServerInstallCount,
|
||||
selectedServerInstallProjects,
|
||||
@@ -143,6 +144,7 @@ const {
|
||||
enforceSetupModpackRoute,
|
||||
getQueuedServerInstallPlans,
|
||||
setQueuedServerInstallPlans,
|
||||
resolveQueuedServerInstallPlan,
|
||||
openServerModpackInstallFlow,
|
||||
onServerFlowBack,
|
||||
handleServerModpackFlowCreate,
|
||||
@@ -870,6 +872,7 @@ function getCardActions(
|
||||
['modpack', 'mod', 'plugin', 'datapack'].includes(currentProjectType)
|
||||
) {
|
||||
const isQueued = queuedServerInstallProjectIds.value.has(projectResult.project_id)
|
||||
const isQueuedRoot = queuedServerInstallRootProjectIds.value.has(projectResult.project_id)
|
||||
const isInstallingSelection = isInstallingQueuedServerInstalls.value
|
||||
const validatingInstall =
|
||||
isInstalling && currentProjectType !== 'modpack' && !isInstallingSelection
|
||||
@@ -897,14 +900,16 @@ function getCardActions(
|
||||
? CheckIcon
|
||||
: PlusIcon,
|
||||
iconClass: isInstalling || isInstallingSelection ? 'animate-spin' : undefined,
|
||||
disabled: showAsInstalled || isInstalling || isInstallingSelection,
|
||||
disabled:
|
||||
showAsInstalled || isInstalling || isInstallingSelection || (isQueued && !isQueuedRoot),
|
||||
color: isQueued && !isInstalling && !isInstallingSelection ? 'green' : 'brand',
|
||||
type: 'outlined',
|
||||
onClick: async () => {
|
||||
if (isQueued) {
|
||||
if (isQueuedRoot) {
|
||||
removeQueuedServerInstall(projectResult.project_id)
|
||||
return
|
||||
}
|
||||
if (isQueued) return
|
||||
|
||||
const contentType = currentProjectType as BrowseInstallContentType
|
||||
const isModpack = contentType === 'modpack'
|
||||
@@ -913,7 +918,7 @@ function getCardActions(
|
||||
setProjectInstalling(projectResult.project_id, true)
|
||||
}
|
||||
try {
|
||||
await requestInstall({
|
||||
const plan = await requestInstall({
|
||||
project: projectResult,
|
||||
contentType,
|
||||
mode: isModpack ? 'immediate' : 'queue',
|
||||
@@ -937,7 +942,9 @@ function getCardActions(
|
||||
iconUrl: plan.project.icon_url ?? undefined,
|
||||
}),
|
||||
})
|
||||
if (!isModpack) await resolveQueuedServerInstallPlan(plan)
|
||||
} catch (err) {
|
||||
if (!isModpack) removeQueuedServerInstall(projectResult.project_id)
|
||||
handleError(err as Error)
|
||||
} finally {
|
||||
if (shouldShowInstalling) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AbstractModrinthClient, Archon, Labrinth } from '@modrinth/api-client'
|
||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
addPendingServerContentInstalls,
|
||||
type BrowseInstallPlan,
|
||||
@@ -6,15 +6,17 @@ import {
|
||||
createContext,
|
||||
type CreationFlowContextValue,
|
||||
flushStoredServerAddonInstallQueue,
|
||||
getServerAddonInstallPlanProjectIds,
|
||||
getStoredServerAddonInstallQueue,
|
||||
getTargetInstallPreferences,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
type ModpackSearchResult,
|
||||
type PendingServerContentInstall,
|
||||
type PendingServerContentInstallType,
|
||||
readPendingServerContentInstalls,
|
||||
readStoredServerInstallQueue,
|
||||
removePendingServerContentInstall,
|
||||
resolveServerAddonInstallPlans,
|
||||
writePendingServerContentInstallBaseline,
|
||||
writeStoredServerInstallQueue,
|
||||
} from '@modrinth/ui'
|
||||
@@ -54,6 +56,7 @@ export interface ServerInstallContentContext {
|
||||
effectiveServerWorldId: ComputedRef<string | null>
|
||||
serverContextServerData: Ref<Archon.Servers.v0.Server | null>
|
||||
serverContentProjectIds: Ref<Set<string>>
|
||||
queuedServerInstallRootProjectIds: ComputedRef<Set<string>>
|
||||
queuedServerInstallProjectIds: ComputedRef<Set<string>>
|
||||
queuedServerInstallCount: ComputedRef<number>
|
||||
selectedServerInstallProjects: ComputedRef<BrowseSelectedProject[]>
|
||||
@@ -76,6 +79,9 @@ export interface ServerInstallContentContext {
|
||||
setQueuedServerInstallPlans: (
|
||||
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
||||
) => void
|
||||
resolveQueuedServerInstallPlan: (
|
||||
plan: BrowseInstallPlan<InstallableSearchResult>,
|
||||
) => Promise<void>
|
||||
openServerModpackInstallFlow: (request: ServerModpackSelectionRequest) => Promise<void>
|
||||
onServerFlowBack: () => void
|
||||
handleServerModpackFlowCreate: (config: CreationFlowContextValue) => Promise<void>
|
||||
@@ -112,48 +118,6 @@ function getQueuedInstallOwnerFallback(project: InstallableSearchResult) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getQueuedInstallOwner(
|
||||
client: AbstractModrinthClient,
|
||||
project: InstallableSearchResult,
|
||||
) {
|
||||
const fallback = getQueuedInstallOwnerFallback(project)
|
||||
|
||||
try {
|
||||
if (project.organization) {
|
||||
const organization = await client.labrinth.projects_v3.getOrganization(project.project_id)
|
||||
if (organization) {
|
||||
return {
|
||||
id: organization.id,
|
||||
name: organization.name,
|
||||
type: 'organization' as const,
|
||||
avatar_url: organization.icon_url ?? undefined,
|
||||
link: `https://modrinth.com/organization/${organization.slug}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const members = await client.labrinth.projects_v3.getMembers(project.project_id)
|
||||
const owner =
|
||||
members.find((member) => member.user.id === project.author_id)?.user ??
|
||||
members.find((member) => member.is_owner || member.role === 'Owner')?.user ??
|
||||
members[0]?.user
|
||||
|
||||
if (owner) {
|
||||
return {
|
||||
id: owner.id,
|
||||
name: owner.username,
|
||||
type: 'user' as const,
|
||||
avatar_url: owner.avatar_url,
|
||||
link: `/user/${encodeURIComponent(owner.username)}`,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
function getQueuedAddonInstallPlans(
|
||||
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
||||
) {
|
||||
@@ -187,17 +151,6 @@ function getQueuedInstallPlaceholderFallbacks(
|
||||
)
|
||||
}
|
||||
|
||||
async function getQueuedInstallPlaceholders(
|
||||
client: AbstractModrinthClient,
|
||||
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
||||
) {
|
||||
return Promise.all(
|
||||
getQueuedAddonInstallPlans(plans).map(async (plan) =>
|
||||
getQueuedInstallPlaceholder(plan, await getQueuedInstallOwner(client, plan.project)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export function createServerInstallContent(opts: {
|
||||
serverSetupModalRef: Ref<ServerSetupModalHandle | null>
|
||||
}) {
|
||||
@@ -250,7 +203,12 @@ export function createServerInstallContent(opts: {
|
||||
const queuedServerInstalls = ref<Map<string, BrowseInstallPlan<InstallableSearchResult>>>(
|
||||
new Map(),
|
||||
)
|
||||
const queuedServerInstallProjectIds = computed(() => new Set(queuedServerInstalls.value.keys()))
|
||||
const queuedServerInstallRootProjectIds = computed(
|
||||
() => new Set(queuedServerInstalls.value.keys()),
|
||||
)
|
||||
const queuedServerInstallProjectIds = computed(() =>
|
||||
getServerAddonInstallPlanProjectIds(queuedServerInstalls.value.values()),
|
||||
)
|
||||
const queuedServerInstallCount = computed(() => queuedServerInstalls.value.size)
|
||||
const selectedServerInstallProjects = computed<BrowseSelectedProject[]>(() =>
|
||||
Array.from(queuedServerInstalls.value.values()).map((plan) => ({
|
||||
@@ -462,6 +420,59 @@ export function createServerInstallContent(opts: {
|
||||
writeStoredServerInstallQueue(serverId, worldId, plans)
|
||||
}
|
||||
|
||||
function toResolvePreferences(
|
||||
preferences?: BrowseInstallPlan<InstallableSearchResult>['preferences'],
|
||||
): Labrinth.Content.v3.ResolutionPreferences {
|
||||
return {
|
||||
game_versions: preferences?.gameVersions,
|
||||
loaders: preferences?.loaders,
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAddonPlan(
|
||||
plan: BrowseInstallPlan<InstallableSearchResult>,
|
||||
existingProjectIds: string[],
|
||||
) {
|
||||
const target = getTargetInstallPreferences(
|
||||
{
|
||||
gameVersion: serverContextServerData.value?.mc_version,
|
||||
loader: serverContextServerData.value?.loader,
|
||||
},
|
||||
plan.contentType,
|
||||
)
|
||||
const resolved = await client.labrinth.content_v3.resolve({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
|
||||
selected: toResolvePreferences(plan.preferences),
|
||||
target: toResolvePreferences(target),
|
||||
existing_project_ids: existingProjectIds,
|
||||
})
|
||||
|
||||
return [resolved.primary, ...resolved.dependencies].map((item) => ({
|
||||
projectId: item.project_id,
|
||||
versionId: item.version_id,
|
||||
}))
|
||||
}
|
||||
|
||||
async function resolveQueuedServerInstallPlan(plan: BrowseInstallPlan<InstallableSearchResult>) {
|
||||
const resolvedContent = await resolveAddonPlan(plan, Array.from(serverContentProjectIds.value))
|
||||
const storedPlan = queuedServerInstalls.value.get(plan.projectId)
|
||||
if (!storedPlan || storedPlan.versionId !== plan.versionId) return
|
||||
|
||||
const nextPlans = new Map(queuedServerInstalls.value)
|
||||
nextPlans.set(plan.projectId, { ...storedPlan, resolvedContent })
|
||||
setQueuedServerInstallPlans(nextPlans)
|
||||
}
|
||||
|
||||
async function resolveQueuedAddonPlans(plans: BrowseInstallPlan<InstallableSearchResult>[]) {
|
||||
return await resolveServerAddonInstallPlans({
|
||||
plans,
|
||||
existingProjectIds: serverContentProjectIds.value,
|
||||
resolvePlan: resolveAddonPlan,
|
||||
})
|
||||
}
|
||||
|
||||
async function flushQueuedServerInstalls(
|
||||
serverId: string | null = serverIdQuery.value,
|
||||
worldId: string | null = effectiveServerWorldId.value,
|
||||
@@ -486,15 +497,12 @@ export function createServerInstallContent(opts: {
|
||||
const result = await flushStoredServerAddonInstallQueue({
|
||||
serverId,
|
||||
worldId,
|
||||
install: (plans) =>
|
||||
client.archon.content_v1.addAddons(
|
||||
serverId,
|
||||
worldId,
|
||||
plans.map((plan) => ({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
})),
|
||||
),
|
||||
install: async (plans) => {
|
||||
const addons = await resolveQueuedAddonPlans(plans)
|
||||
if (addons.length > 0) {
|
||||
await client.archon.content_v1.addAddons(serverId, worldId, addons)
|
||||
}
|
||||
},
|
||||
onQueueChange: (plans) => setStoredServerInstallPlans(serverId, worldId, plans),
|
||||
})
|
||||
|
||||
@@ -544,21 +552,10 @@ export function createServerInstallContent(opts: {
|
||||
writeStoredServerInstallQueue(sid, wid, plans)
|
||||
writePendingServerContentInstallBaseline(sid, wid, serverContentInstallKeys.value)
|
||||
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
|
||||
void getQueuedInstallPlaceholders(client, plans)
|
||||
.then((items) => {
|
||||
const pendingProjectIds = new Set(
|
||||
readPendingServerContentInstalls(sid, wid).map((item) => item.projectId),
|
||||
)
|
||||
addPendingServerContentInstalls(
|
||||
sid,
|
||||
wid,
|
||||
items.filter((item) => pendingProjectIds.has(item.projectId)),
|
||||
)
|
||||
})
|
||||
.catch((err) => handleError(err as Error))
|
||||
}
|
||||
const installed = await flushQueuedServerInstalls(sid, wid)
|
||||
if (!installed) return false
|
||||
await router.push(backUrl)
|
||||
void flushQueuedServerInstalls(sid, wid)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -627,6 +624,7 @@ export function createServerInstallContent(opts: {
|
||||
effectiveServerWorldId,
|
||||
serverContextServerData,
|
||||
serverContentProjectIds,
|
||||
queuedServerInstallRootProjectIds,
|
||||
queuedServerInstallProjectIds,
|
||||
queuedServerInstallCount,
|
||||
selectedServerInstallProjects,
|
||||
@@ -647,6 +645,7 @@ export function createServerInstallContent(opts: {
|
||||
enforceSetupModpackRoute,
|
||||
getQueuedServerInstallPlans,
|
||||
setQueuedServerInstallPlans,
|
||||
resolveQueuedServerInstallPlan,
|
||||
openServerModpackInstallFlow,
|
||||
onServerFlowBack,
|
||||
handleServerModpackFlowCreate,
|
||||
|
||||
@@ -14,14 +14,15 @@ import {
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
flushStoredServerAddonInstallQueue,
|
||||
getServerAddonInstallPlanProjectIds,
|
||||
getStoredServerAddonInstallQueue,
|
||||
getTargetInstallPreferences,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
readPendingServerContentInstalls,
|
||||
readStoredServerInstallQueue,
|
||||
removePendingServerContentInstall,
|
||||
requestInstall,
|
||||
resolveServerAddonInstallPlans,
|
||||
stripServerRuntimeInstallFilters,
|
||||
stripServerRuntimeInstallOverrides,
|
||||
useVIntl,
|
||||
@@ -177,7 +178,12 @@ export function useServerInstallContent({
|
||||
const queuedServerInstalls = ref<Map<string, BrowseInstallPlan<ServerInstallSearchResult>>>(
|
||||
readStoredServerInstallQueue(currentServerId.value, currentWorldId.value),
|
||||
)
|
||||
const queuedServerInstallProjectIds = computed(() => new Set(queuedServerInstalls.value.keys()))
|
||||
const queuedServerInstallRootProjectIds = computed(
|
||||
() => new Set(queuedServerInstalls.value.keys()),
|
||||
)
|
||||
const queuedServerInstallProjectIds = computed(() =>
|
||||
getServerAddonInstallPlanProjectIds(queuedServerInstalls.value.values()),
|
||||
)
|
||||
const queuedServerInstallCount = computed(() => queuedServerInstalls.value.size)
|
||||
const selectedServerInstallProjects = computed(() =>
|
||||
Array.from(queuedServerInstalls.value.values()).map((plan) => ({
|
||||
@@ -219,45 +225,6 @@ export function useServerInstallContent({
|
||||
writeStoredServerInstallQueue(serverId, worldId, plans)
|
||||
}
|
||||
|
||||
async function getQueuedInstallOwner(project: ServerInstallSearchResult) {
|
||||
const fallback = getQueuedInstallOwnerFallback(project)
|
||||
|
||||
try {
|
||||
if (project.organization) {
|
||||
const organization = await client.labrinth.projects_v3.getOrganization(project.project_id)
|
||||
if (organization) {
|
||||
return {
|
||||
id: organization.id,
|
||||
name: organization.name,
|
||||
type: 'organization' as const,
|
||||
avatar_url: organization.icon_url ?? undefined,
|
||||
link: `/organization/${organization.slug}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const members = await client.labrinth.projects_v3.getMembers(project.project_id)
|
||||
const owner =
|
||||
members.find((member) => member.user.id === project.author_id)?.user ??
|
||||
members.find((member) => member.is_owner || member.role === 'Owner')?.user ??
|
||||
members[0]?.user
|
||||
|
||||
if (owner) {
|
||||
return {
|
||||
id: owner.id,
|
||||
name: owner.username,
|
||||
type: 'user' as const,
|
||||
avatar_url: owner.avatar_url,
|
||||
link: `/user/${owner.username}`,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
function getQueuedInstallPlaceholder(
|
||||
plan: BrowseInstallPlan<ServerInstallSearchResult>,
|
||||
owner: PendingServerContentInstallInput['owner'],
|
||||
@@ -284,16 +251,6 @@ export function useServerInstallContent({
|
||||
)
|
||||
}
|
||||
|
||||
async function getQueuedInstallPlaceholders(
|
||||
plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>,
|
||||
) {
|
||||
return Promise.all(
|
||||
getQueuedAddonInstallPlans(plans).map(async (plan) =>
|
||||
getQueuedInstallPlaceholder(plan, await getQueuedInstallOwner(plan.project)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function setProjectInstalling(projectId: string, installing: boolean) {
|
||||
const next = new Set(installingProjectIds.value)
|
||||
if (installing) {
|
||||
@@ -437,32 +394,43 @@ export function useServerInstallContent({
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAddonPlan(
|
||||
plan: BrowseInstallPlan<ServerInstallSearchResult>,
|
||||
existingProjectIds: string[],
|
||||
) {
|
||||
const resolved = await client.labrinth.content_v3.resolve({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
|
||||
selected: toResolvePreferences(plan.preferences),
|
||||
target: toResolvePreferences(getServerInstallTargetPreferences(plan.contentType)),
|
||||
existing_project_ids: existingProjectIds,
|
||||
})
|
||||
|
||||
return [resolved.primary, ...resolved.dependencies].map((item) => ({
|
||||
projectId: item.project_id,
|
||||
versionId: item.version_id,
|
||||
}))
|
||||
}
|
||||
|
||||
async function resolveAndStoreQueuedAddonPlan(
|
||||
plan: BrowseInstallPlan<ServerInstallSearchResult>,
|
||||
) {
|
||||
const resolvedContent = await resolveAddonPlan(plan, Array.from(getServerInstalledProjectIds()))
|
||||
const storedPlan = queuedServerInstalls.value.get(plan.projectId)
|
||||
if (!storedPlan || storedPlan.versionId !== plan.versionId) return
|
||||
|
||||
const nextPlans = new Map(queuedServerInstalls.value)
|
||||
nextPlans.set(plan.projectId, { ...storedPlan, resolvedContent })
|
||||
serverInstallQueue.set(nextPlans)
|
||||
}
|
||||
|
||||
async function resolveQueuedAddonPlans(plans: BrowseInstallPlan<ServerInstallSearchResult>[]) {
|
||||
const existingProjectIds = getServerInstalledProjectIds()
|
||||
const resolvedAddons: Array<{ project_id: string; version_id: string }> = []
|
||||
|
||||
for (const plan of plans) {
|
||||
const resolved = await client.labrinth.content_v3.resolve({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
|
||||
selected: toResolvePreferences(plan.preferences),
|
||||
target: toResolvePreferences(getServerInstallTargetPreferences(plan.contentType)),
|
||||
existing_project_ids: Array.from(existingProjectIds),
|
||||
})
|
||||
const content = [resolved.primary, ...resolved.dependencies]
|
||||
|
||||
for (const item of content) {
|
||||
if (existingProjectIds.has(item.project_id)) continue
|
||||
existingProjectIds.add(item.project_id)
|
||||
resolvedAddons.push({
|
||||
project_id: item.project_id,
|
||||
version_id: item.version_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedAddons
|
||||
return await resolveServerAddonInstallPlans({
|
||||
plans,
|
||||
existingProjectIds: getServerInstalledProjectIds(),
|
||||
resolvePlan: resolveAddonPlan,
|
||||
})
|
||||
}
|
||||
|
||||
function getInstallProjectVersions(projectId: string) {
|
||||
@@ -533,10 +501,9 @@ export function useServerInstallContent({
|
||||
total: result.flushedPlans.length,
|
||||
}
|
||||
if (result.flushedPlans.length > 0) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', serverId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['content', 'list'] }),
|
||||
])
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['content', 'list', 'v1', serverId],
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -564,21 +531,10 @@ export function useServerInstallContent({
|
||||
...optimisticallyInstalledProjectIds.value,
|
||||
])
|
||||
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
|
||||
void getQueuedInstallPlaceholders(plans)
|
||||
.then((items) => {
|
||||
const pendingProjectIds = new Set(
|
||||
readPendingServerContentInstalls(sid, wid).map((item) => item.projectId),
|
||||
)
|
||||
addPendingServerContentInstalls(
|
||||
sid,
|
||||
wid,
|
||||
items.filter((item) => pendingProjectIds.has(item.projectId)),
|
||||
)
|
||||
})
|
||||
.catch((err) => handleError(err as Error))
|
||||
}
|
||||
const installed = await flushQueuedServerInstalls(sid, wid)
|
||||
if (!installed) return false
|
||||
await navigateTo(backUrl)
|
||||
void flushQueuedServerInstalls(sid, wid)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -598,16 +554,17 @@ export function useServerInstallContent({
|
||||
const isModpack = contentType === 'modpack'
|
||||
|
||||
try {
|
||||
if (!isModpack && queuedServerInstallProjectIds.value.has(project.project_id)) {
|
||||
if (!isModpack && queuedServerInstallRootProjectIds.value.has(project.project_id)) {
|
||||
removeQueuedServerInstall(project.project_id)
|
||||
return
|
||||
}
|
||||
if (!isModpack && queuedServerInstallProjectIds.value.has(project.project_id)) return
|
||||
|
||||
if (isModpack || !queuedServerInstallProjectIds.value.has(project.project_id)) {
|
||||
setProjectInstalling(project.project_id, true)
|
||||
}
|
||||
|
||||
await requestInstall({
|
||||
const plan = await requestInstall({
|
||||
project,
|
||||
contentType,
|
||||
mode: isModpack ? 'immediate' : 'queue',
|
||||
@@ -646,10 +603,13 @@ export function useServerInstallContent({
|
||||
ctx.modal.value?.setStage('final-config')
|
||||
},
|
||||
})
|
||||
if (!isModpack) await resolveAndStoreQueuedAddonPlan(plan)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
if (isModpack) {
|
||||
setProjectInstalling(project.project_id, false)
|
||||
} else {
|
||||
removeQueuedServerInstall(project.project_id)
|
||||
}
|
||||
handleError(e instanceof Error ? e : new Error(`Error installing content ${e}`))
|
||||
} finally {
|
||||
@@ -804,6 +764,7 @@ export function useServerInstallContent({
|
||||
hideSelectedServerInstalls,
|
||||
installingProjectIds,
|
||||
optimisticallyInstalledProjectIds,
|
||||
queuedServerInstallRootProjectIds,
|
||||
queuedServerInstallProjectIds,
|
||||
queuedServerInstallCount,
|
||||
isInstallingQueuedServerInstalls,
|
||||
|
||||
@@ -163,6 +163,7 @@ const {
|
||||
hideSelectedServerInstalls,
|
||||
installingProjectIds,
|
||||
optimisticallyInstalledProjectIds,
|
||||
queuedServerInstallRootProjectIds,
|
||||
queuedServerInstallProjectIds,
|
||||
queuedServerInstallCount,
|
||||
isInstallingQueuedServerInstalls,
|
||||
@@ -327,6 +328,7 @@ function getCardActions(
|
||||
|
||||
if (serverData.value) {
|
||||
const isQueued = queuedServerInstallProjectIds.value.has(result.project_id)
|
||||
const isQueuedRoot = queuedServerInstallRootProjectIds.value.has(result.project_id)
|
||||
const isInstalled =
|
||||
projectResult.installed ||
|
||||
optimisticallyInstalledProjectIds.value.has(result.project_id) ||
|
||||
@@ -362,7 +364,8 @@ function getCardActions(
|
||||
? CheckIcon
|
||||
: DownloadIcon,
|
||||
iconClass: isInstalling || isInstallingSelection ? 'animate-spin' : undefined,
|
||||
disabled: !!isInstalled || isInstalling || isInstallingSelection,
|
||||
disabled:
|
||||
!!isInstalled || isInstalling || isInstallingSelection || (isQueued && !isQueuedRoot),
|
||||
color: isQueued && !isInstalling && !isInstallingSelection ? 'green' : 'brand',
|
||||
type: 'outlined',
|
||||
onClick: () => serverInstall(projectResult),
|
||||
|
||||
@@ -7,6 +7,10 @@ export type UploadSessionFile = {
|
||||
filename: string
|
||||
}
|
||||
|
||||
function getUploadSessionPath(scope: Kyros.UploadSessions.v1.Scope, worldId: string) {
|
||||
return `/worlds/${worldId}/${scope}/upload-session`
|
||||
}
|
||||
|
||||
export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'kyros_upload_sessions_v1'
|
||||
@@ -17,7 +21,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||
worldId: string,
|
||||
): Promise<Kyros.UploadSessions.v1.UploadSessionResponse> {
|
||||
return this.client.request<Kyros.UploadSessions.v1.UploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session`,
|
||||
getUploadSessionPath(scope, worldId),
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
@@ -32,7 +36,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||
worldId: string,
|
||||
): Promise<Kyros.UploadSessions.v1.GetUploadSessionResponse> {
|
||||
return this.client.request<Kyros.UploadSessions.v1.GetUploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session`,
|
||||
getUploadSessionPath(scope, worldId),
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
@@ -58,7 +62,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||
}
|
||||
|
||||
return this.client.upload<Kyros.UploadSessions.v1.UploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session/${uploadId}/files`,
|
||||
`${getUploadSessionPath(scope, worldId)}/${uploadId}/files`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
@@ -76,7 +80,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||
uploadId: string,
|
||||
): Promise<Kyros.UploadSessions.v1.UploadSessionResponse> {
|
||||
return this.client.request<Kyros.UploadSessions.v1.UploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session/${uploadId}/finalize`,
|
||||
`${getUploadSessionPath(scope, worldId)}/${uploadId}/finalize`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
@@ -92,7 +96,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||
uploadId: string,
|
||||
): Promise<Kyros.UploadSessions.v1.UploadSessionResponse> {
|
||||
return this.client.request<Kyros.UploadSessions.v1.UploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session/${uploadId}`,
|
||||
`${getUploadSessionPath(scope, worldId)}/${uploadId}`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
|
||||
@@ -68,6 +68,7 @@ useQuery({
|
||||
from_modpack: false,
|
||||
}),
|
||||
enabled: computed(() => !!worldId.value),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const serverSettingsTabComponentMap = {
|
||||
|
||||
@@ -199,7 +199,6 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
||||
}
|
||||
: current,
|
||||
)
|
||||
void queryClient.invalidateQueries({ queryKey: contentListKey(serverId) })
|
||||
}
|
||||
|
||||
function handleWorldContentBaseUpdate(
|
||||
|
||||
@@ -31,6 +31,11 @@ export interface BrowseInstallTarget {
|
||||
loader?: string | null
|
||||
}
|
||||
|
||||
export interface BrowseResolvedInstallContent {
|
||||
projectId: string
|
||||
versionId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal project shape needed by shared install resolution.
|
||||
*/
|
||||
@@ -59,6 +64,7 @@ export interface BrowseInstallPlan<TProject extends BrowseInstallProject = Brows
|
||||
contentType: BrowseInstallContentType
|
||||
preferences: BrowseInstallPreferences
|
||||
source: BrowseInstallPlanSource
|
||||
resolvedContent?: BrowseResolvedInstallContent[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,6 +341,15 @@ export interface FlushStoredServerAddonInstallQueueOptions<TProject extends Brow
|
||||
onQueueChange?: (plans: Map<string, BrowseInstallPlan<TProject>>) => void
|
||||
}
|
||||
|
||||
export interface ResolveServerAddonInstallPlansOptions<TProject extends BrowseInstallProject> {
|
||||
plans: readonly BrowseInstallPlan<TProject>[]
|
||||
existingProjectIds: Iterable<string>
|
||||
resolvePlan: (
|
||||
plan: BrowseInstallPlan<TProject>,
|
||||
existingProjectIds: string[],
|
||||
) => Promise<BrowseResolvedInstallContent[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a queue flush. Failed plans are also written back to the queue.
|
||||
*/
|
||||
@@ -646,6 +661,62 @@ export function getStoredServerAddonInstallQueue<
|
||||
return addonPlans
|
||||
}
|
||||
|
||||
export function getServerAddonInstallPlanProjectIds<TProject extends BrowseInstallProject>(
|
||||
plans: Iterable<BrowseInstallPlan<TProject>>,
|
||||
) {
|
||||
const projectIds = new Set<string>()
|
||||
for (const plan of plans) {
|
||||
projectIds.add(plan.projectId)
|
||||
for (const item of plan.resolvedContent ?? []) {
|
||||
projectIds.add(item.projectId)
|
||||
}
|
||||
}
|
||||
return projectIds
|
||||
}
|
||||
|
||||
export async function resolveServerAddonInstallPlans<TProject extends BrowseInstallProject>({
|
||||
plans,
|
||||
existingProjectIds,
|
||||
resolvePlan,
|
||||
}: ResolveServerAddonInstallPlansOptions<TProject>) {
|
||||
const installedProjectIds = new Set(existingProjectIds)
|
||||
const resolutionExistingProjectIds = Array.from(installedProjectIds)
|
||||
const resolvedPlans = await Promise.all(
|
||||
plans.map(
|
||||
async (plan) =>
|
||||
plan.resolvedContent ?? (await resolvePlan(plan, resolutionExistingProjectIds)),
|
||||
),
|
||||
)
|
||||
const explicitProjectIds = new Set(plans.map((plan) => plan.projectId))
|
||||
const addons = new Map<string, { project_id: string; version_id: string }>()
|
||||
|
||||
for (const plan of plans) {
|
||||
if (installedProjectIds.has(plan.projectId)) continue
|
||||
addons.set(plan.projectId, {
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
})
|
||||
}
|
||||
|
||||
for (const content of resolvedPlans) {
|
||||
for (const item of content) {
|
||||
if (
|
||||
installedProjectIds.has(item.projectId) ||
|
||||
explicitProjectIds.has(item.projectId) ||
|
||||
addons.has(item.projectId)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
addons.set(item.projectId, {
|
||||
project_id: item.projectId,
|
||||
version_id: item.versionId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(addons.values())
|
||||
}
|
||||
|
||||
export async function flushStoredServerAddonInstallQueue<TProject extends BrowseInstallProject>({
|
||||
serverId,
|
||||
worldId,
|
||||
@@ -869,7 +940,22 @@ function isStoredBrowseInstallPlan(
|
||||
typeof record.versionId === 'string' &&
|
||||
isStoredBrowseInstallContentType(record.contentType) &&
|
||||
isStoredBrowseInstallPreferences(record.preferences) &&
|
||||
(record.source === 'filtered' || record.source === 'target')
|
||||
(record.source === 'filtered' || record.source === 'target') &&
|
||||
isOptionalStoredResolvedContent(record.resolvedContent)
|
||||
)
|
||||
}
|
||||
|
||||
function isOptionalStoredResolvedContent(value: unknown) {
|
||||
return (
|
||||
value === undefined ||
|
||||
(Array.isArray(value) &&
|
||||
value.every(
|
||||
(item) =>
|
||||
!!item &&
|
||||
typeof item === 'object' &&
|
||||
typeof (item as Record<string, unknown>).projectId === 'string' &&
|
||||
typeof (item as Record<string, unknown>).versionId === 'string',
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +245,7 @@ const addonsQuery = useQuery({
|
||||
queryFn: () =>
|
||||
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
|
||||
enabled: computed(() => worldId.value !== null),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const modpack = computed(() => addonsQuery.data.value?.modpack ?? null)
|
||||
@@ -411,6 +412,9 @@ async function uploadLocalModpackWithSoftOverride() {
|
||||
|
||||
provideInstallationSettings({
|
||||
closeSettings: serverSettings.closeModal,
|
||||
afterSave: async () => {
|
||||
serverSettings.closeModal?.()
|
||||
},
|
||||
onGameVersionHover: handleGameVersionHover,
|
||||
loading: computed(() => !server.value || addonsQuery.isLoading.value),
|
||||
installationInfo: computed(() => {
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
flushStoredServerAddonInstallQueue,
|
||||
getStoredServerAddonInstallQueue,
|
||||
getTargetInstallPreferences,
|
||||
resolveServerAddonInstallPlans,
|
||||
} from '../../../shared/browse-tab/composables/install-logic'
|
||||
import ManagedContentModal from '../../../shared/content-tab/components/managed-content-modal/index.vue'
|
||||
import ConfirmModpackUpdateModal from '../../../shared/content-tab/components/modals/ConfirmModpackUpdateModal.vue'
|
||||
@@ -157,7 +158,7 @@ const contentQuery = useQuery({
|
||||
queryFn: () =>
|
||||
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
|
||||
enabled: computed(() => worldId.value !== null),
|
||||
staleTime: 0,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const isModpackContentModalOpen = ref(false)
|
||||
@@ -168,7 +169,7 @@ const modpackContentQuery = useQuery({
|
||||
from_modpack: true,
|
||||
}),
|
||||
enabled: computed(() => worldId.value !== null && !!contentQuery.data.value?.modpack),
|
||||
staleTime: 0,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const setupActionDisabled = computed(() => !canSetup.value || busyReasons.value.length > 0)
|
||||
@@ -395,37 +396,32 @@ function toResolvePreferences(
|
||||
}
|
||||
|
||||
async function resolveStoredServerAddonPlans(plans: BrowseInstallPlan[]) {
|
||||
const existingProjectIds = getInstalledProjectIds()
|
||||
const resolvedAddons: Array<{ project_id: string; version_id: string }> = []
|
||||
|
||||
for (const plan of plans) {
|
||||
const target = getTargetInstallPreferences(
|
||||
{
|
||||
gameVersion: server.value?.mc_version,
|
||||
loader: server.value?.loader,
|
||||
},
|
||||
plan.contentType,
|
||||
)
|
||||
const resolved = await client.labrinth.content_v3.resolve({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
|
||||
selected: toResolvePreferences(plan.preferences),
|
||||
target: toResolvePreferences(target),
|
||||
existing_project_ids: Array.from(existingProjectIds),
|
||||
})
|
||||
|
||||
for (const item of [resolved.primary, ...resolved.dependencies]) {
|
||||
if (existingProjectIds.has(item.project_id)) continue
|
||||
existingProjectIds.add(item.project_id)
|
||||
resolvedAddons.push({
|
||||
project_id: item.project_id,
|
||||
version_id: item.version_id,
|
||||
return await resolveServerAddonInstallPlans({
|
||||
plans,
|
||||
existingProjectIds: getInstalledProjectIds(),
|
||||
resolvePlan: async (plan, existingProjectIds) => {
|
||||
const target = getTargetInstallPreferences(
|
||||
{
|
||||
gameVersion: server.value?.mc_version,
|
||||
loader: server.value?.loader,
|
||||
},
|
||||
plan.contentType,
|
||||
)
|
||||
const resolved = await client.labrinth.content_v3.resolve({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
|
||||
selected: toResolvePreferences(plan.preferences),
|
||||
target: toResolvePreferences(target),
|
||||
existing_project_ids: existingProjectIds,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedAddons
|
||||
return [resolved.primary, ...resolved.dependencies].map((item) => ({
|
||||
projectId: item.project_id,
|
||||
versionId: item.version_id,
|
||||
}))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function addonMatchesPendingInstall(
|
||||
|
||||
@@ -1124,8 +1124,13 @@ const handleFilesystemOps = (data: Archon.Websocket.v0.WSFilesystemOpsEvent) =>
|
||||
)
|
||||
}
|
||||
|
||||
let newModInvalidateTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleNewMod = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
|
||||
if (newModInvalidateTimer) clearTimeout(newModInvalidateTimer)
|
||||
newModInvalidateTimer = setTimeout(() => {
|
||||
newModInvalidateTimer = null
|
||||
void queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleInstallationResult = async (data: Archon.Websocket.v0.WSInstallationResultEvent) => {
|
||||
@@ -1520,6 +1525,10 @@ function initializeServer() {
|
||||
|
||||
const cleanup = () => {
|
||||
isMounted.value = false
|
||||
if (newModInvalidateTimer) {
|
||||
clearTimeout(newModInvalidateTimer)
|
||||
newModInvalidateTimer = null
|
||||
}
|
||||
|
||||
saveWsStateToCache()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user