feat:移除了弹窗,服务器添加sls
This commit is contained in:
454
apps/app-frontend/src/providers/setup/creation-modal.ts
Normal file
454
apps/app-frontend/src/providers/setup/creation-modal.ts
Normal file
@ -0,0 +1,454 @@
|
||||
import type {
|
||||
AbstractPopupNotificationManager,
|
||||
AbstractWebNotificationManager,
|
||||
CreationFlowContextValue,
|
||||
CreationFlowModal,
|
||||
SymlinkMethodChoice,
|
||||
} from '@modrinth/ui'
|
||||
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { join } from '@tauri-apps/api/path'
|
||||
import { inject, provide, ref, useTemplateRef } from 'vue'
|
||||
import type { ComponentExposed } from 'vue-component-type-helpers'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import type UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
|
||||
import type ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project_versions, get_search_results } from '@/helpers/cache.js'
|
||||
import { getCurseForgeFiles, hasCompatibleCurseForgeFile } from '@/helpers/curseforge'
|
||||
import { install_job_listener } from '@/helpers/events.js'
|
||||
import { import_instance } from '@/helpers/import.js'
|
||||
import {
|
||||
type CreatePackLocation,
|
||||
install_create_instance,
|
||||
install_create_modpack_instance,
|
||||
install_get_modpack_preview,
|
||||
type InstallJobSnapshot,
|
||||
wait_for_install_job,
|
||||
} from '@/helpers/install'
|
||||
import { check_symlink_capability, list } from '@/helpers/instance'
|
||||
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata.js'
|
||||
import type { InstanceLoader } from '@/helpers/types'
|
||||
|
||||
const symlinkMessages = defineMessages({
|
||||
unsupportedTitle: {
|
||||
id: 'app.symlink-capability.unsupported.title',
|
||||
defaultMessage: 'Shared instances are unavailable',
|
||||
},
|
||||
unsupportedBody: {
|
||||
id: 'app.symlink-capability.unsupported',
|
||||
defaultMessage: 'This system does not support creating symbolic links.',
|
||||
},
|
||||
})
|
||||
|
||||
const modpackMessages = defineMessages({
|
||||
installing: {
|
||||
id: 'app.drop.modpack-installing',
|
||||
defaultMessage: 'Installing modpack...',
|
||||
},
|
||||
installed: {
|
||||
id: 'app.drop.modpack-installed-success',
|
||||
defaultMessage: 'Modpack installed successfully',
|
||||
},
|
||||
installingFile: {
|
||||
id: 'app.drop.installing-file',
|
||||
defaultMessage: 'Installing {name}...',
|
||||
},
|
||||
unknownFileType: {
|
||||
id: 'app.drop.unknown-force-analysis-title',
|
||||
defaultMessage: 'Unable to identify file type',
|
||||
},
|
||||
unknownFileTypeText: {
|
||||
id: 'app.drop.unknown-force-analysis-text',
|
||||
defaultMessage:
|
||||
'This archive needs to be extracted and deeply analyzed to determine its content type. This may take a while. Force analysis?',
|
||||
},
|
||||
forceAnalysis: {
|
||||
id: 'app.drop.unknown-force-analysis-button',
|
||||
defaultMessage: 'Force analysis',
|
||||
},
|
||||
analyzing: {
|
||||
id: 'app.drop.unknown-force-analyzing',
|
||||
defaultMessage: 'Force analyzing archive...',
|
||||
},
|
||||
couldNotIdentify: {
|
||||
id: 'app.drop.unknown-force-analysis-failed-title',
|
||||
defaultMessage: 'Analysis failed',
|
||||
},
|
||||
couldNotIdentifyText: {
|
||||
id: 'app.drop.unknown-force-analysis-failed-text',
|
||||
defaultMessage: 'Could not identify the file type even after deep analysis.',
|
||||
},
|
||||
unexpectedType: {
|
||||
id: 'app.drop.unexpected-type',
|
||||
defaultMessage: 'Unexpected type: {type}',
|
||||
},
|
||||
})
|
||||
|
||||
const OPTIFABRIC_CURSEFORGE_PROJECT_ID = 322385
|
||||
|
||||
export function setupCreationModal(
|
||||
notificationManager: AbstractWebNotificationManager,
|
||||
_popupNotificationManager: AbstractPopupNotificationManager,
|
||||
) {
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError } = notificationManager
|
||||
const router = useRouter()
|
||||
|
||||
const installationModal =
|
||||
useTemplateRef<ComponentExposed<typeof CreationFlowModal>>('installationModal')
|
||||
const unknownPackWarningModal =
|
||||
useTemplateRef<InstanceType<typeof UnknownPackWarningModal>>('unknownPackWarningModal')
|
||||
const modpackAlreadyInstalledModal = ref<InstanceType<typeof ModpackAlreadyInstalledModal>>()
|
||||
|
||||
function setModpackAlreadyInstalledModal(
|
||||
modal: InstanceType<typeof ModpackAlreadyInstalledModal>,
|
||||
) {
|
||||
modpackAlreadyInstalledModal.value = modal
|
||||
}
|
||||
|
||||
async function fetchExistingInstanceNames(): Promise<string[]> {
|
||||
const instances = await list().catch(handleError)
|
||||
return instances?.map((i) => i.name) ?? []
|
||||
}
|
||||
|
||||
provide('showCreationModal', () => {
|
||||
installationModal.value?.show()
|
||||
})
|
||||
|
||||
provide(
|
||||
'showCreationModalWithOptions',
|
||||
(options?: {
|
||||
skipSetupType?: boolean
|
||||
initialMode?: 'custom' | 'import'
|
||||
onBack?: () => void
|
||||
}) => {
|
||||
installationModal.value?.show(options)
|
||||
},
|
||||
)
|
||||
|
||||
async function proceedWithModpackCreation(
|
||||
projectId: string,
|
||||
versionId: string,
|
||||
name: string,
|
||||
iconUrl?: string,
|
||||
) {
|
||||
await install_create_modpack_instance(
|
||||
{
|
||||
type: 'fromVersionId',
|
||||
project_id: projectId,
|
||||
version_id: versionId,
|
||||
title: name,
|
||||
icon_url: iconUrl,
|
||||
},
|
||||
{ name },
|
||||
).catch(handleError)
|
||||
trackEvent('InstanceCreate', { source: 'CreationModalModpack' })
|
||||
}
|
||||
|
||||
async function handleCreate(config: CreationFlowContextValue) {
|
||||
try {
|
||||
installationModal.value?.hide()
|
||||
|
||||
if (config.isImportMode.value) {
|
||||
// Collect all instances to import
|
||||
const instanceEntries: Array<{
|
||||
launcherType: string
|
||||
launcherName: string
|
||||
path: string
|
||||
instanceName: string
|
||||
instancePath: string
|
||||
}> = []
|
||||
for (const [launcherName, instanceSet] of Object.entries(
|
||||
config.importSelectedInstances.value,
|
||||
)) {
|
||||
const launcher = config.importLaunchers.value.find((l) => l.name === launcherName)
|
||||
if (!launcher || instanceSet.size === 0) continue
|
||||
for (const name of instanceSet) {
|
||||
const instanceData = launcher.instances.find((i) => i.name === name)
|
||||
instanceEntries.push({
|
||||
launcherType: launcher.launcherType ?? launcher.name,
|
||||
launcherName: launcher.name,
|
||||
path: launcher.path,
|
||||
instanceName: name,
|
||||
instancePath: instanceData?.path ?? '',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (instanceEntries.length === 0) return
|
||||
|
||||
// Show SymlinkMethodCards for user to choose copy vs symlink
|
||||
const capability = await check_symlink_capability()
|
||||
if (capability === 'unsupported') {
|
||||
notificationManager.addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(symlinkMessages.unsupportedTitle),
|
||||
text: formatMessage(symlinkMessages.unsupportedBody),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const chooseImportMethod: (options: {
|
||||
instanceNames: string[]
|
||||
symlinkCapable: 'supported' | 'requires_admin' | 'unsupported'
|
||||
}) => Promise<SymlinkMethodChoice[]> = inject('chooseImportMethod')!
|
||||
|
||||
const choices = await chooseImportMethod({
|
||||
instanceNames: instanceEntries.map((e) => e.instanceName),
|
||||
symlinkCapable: capability,
|
||||
})
|
||||
|
||||
if (choices.length === 0) return
|
||||
|
||||
const choiceByInstanceName = new Map(choices.map((choice) => [choice.instanceName, choice]))
|
||||
|
||||
for (const entry of instanceEntries) {
|
||||
const choice = choiceByInstanceName.get(entry.instanceName)
|
||||
try {
|
||||
const job = await import_instance(
|
||||
entry.launcherType,
|
||||
entry.path,
|
||||
entry.instanceName,
|
||||
choice?.symlink ?? false,
|
||||
entry.instancePath,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
choice?.gameDirOverride ?? null,
|
||||
)
|
||||
await wait_for_install_job(job.job_id)
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
trackEvent('InstanceCreate', { source: 'CreationModalImport' })
|
||||
return
|
||||
}
|
||||
|
||||
if (config.modpackSelection.value) {
|
||||
const { projectId, versionId, name, iconUrl } = config.modpackSelection.value
|
||||
await proceedWithModpackCreation(projectId, versionId, name, iconUrl)
|
||||
return
|
||||
}
|
||||
|
||||
if (config.modpackFilePath.value) {
|
||||
// Fallback: called when modpack is imported via the creation flow
|
||||
// (not via onImportFileReceived, which has its own install path).
|
||||
const splitPath = config.modpackFilePath.value.split(/[\\/]/)
|
||||
const fileName = splitPath ? splitPath[splitPath.length - 1] : config.modpackFilePath.value
|
||||
await installModpackFromPath(config.modpackFilePath.value, fileName)
|
||||
trackEvent('InstanceCreate', { source: 'CreationModalModpackFile' })
|
||||
return
|
||||
}
|
||||
|
||||
// Custom/vanilla setup
|
||||
const loader = config.hideLoaderChips.value
|
||||
? 'vanilla'
|
||||
: (config.selectedLoader.value ?? 'vanilla')
|
||||
const loaderVersion = config.hideLoaderVersion.value
|
||||
? null
|
||||
: (config.selectedLoaderVersion.value ?? config.loaderVersionType.value)
|
||||
const iconPath = config.instanceIconPath.value ?? null
|
||||
const name = config.instanceName.value.trim() || config.autoInstanceName.value
|
||||
// Game directory: `gameDirOverride` holds the picked `.minecraft`
|
||||
// root. Builtin keeps the managed folder (null); external resolves to
|
||||
// `<root>/versions/<name>` when version-isolated, or the `.minecraft`
|
||||
// root itself when not.
|
||||
const mode = config.gameDirOverrideMode.value
|
||||
const gameRoot = config.gameDirOverride.value ?? null
|
||||
const gameDirOverride =
|
||||
mode === 'builtin'
|
||||
? null
|
||||
: gameRoot
|
||||
? mode === 'isolated'
|
||||
? await join(gameRoot, 'versions', name)
|
||||
: gameRoot
|
||||
: null
|
||||
|
||||
await install_create_instance({
|
||||
name,
|
||||
gameVersion: config.selectedGameVersion.value!,
|
||||
loader: loader as InstanceLoader,
|
||||
loaderVersion,
|
||||
adjuncts: config.selectedAdjuncts.value.map((kind) => ({
|
||||
instanceId: '',
|
||||
kind,
|
||||
version: null,
|
||||
role: 'adjunct',
|
||||
})),
|
||||
iconPath,
|
||||
gameDirOverride,
|
||||
}).catch(handleError)
|
||||
|
||||
trackEvent('InstanceCreate', {
|
||||
source: 'CreationModal',
|
||||
})
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
}
|
||||
}
|
||||
|
||||
const pendingModpackCreation = ref<{
|
||||
projectId: string
|
||||
versionId: string
|
||||
name: string
|
||||
iconUrl?: string
|
||||
} | null>(null)
|
||||
|
||||
async function doInstallModpackFile(
|
||||
location: CreatePackLocation,
|
||||
options: { autoCloseMs?: number | null; existingNotify?: { id: number } } = {},
|
||||
) {
|
||||
const existingNotify = options.existingNotify
|
||||
const installingNotify =
|
||||
existingNotify ??
|
||||
notificationManager.addNotification({
|
||||
title: formatMessage(modpackMessages.installing),
|
||||
type: 'info',
|
||||
autoCloseMs: options.autoCloseMs ?? 1000 * 10,
|
||||
})
|
||||
|
||||
const job = await install_create_modpack_instance(location).catch((e) => {
|
||||
notificationManager.removeNotification(installingNotify.id)
|
||||
handleError(e)
|
||||
return null
|
||||
})
|
||||
if (!job) return
|
||||
|
||||
// Single-use listener that auto-cleans up when the job reaches a terminal state
|
||||
const unlisten = await install_job_listener((updatedJob: InstallJobSnapshot) => {
|
||||
if (updatedJob.job_id !== job.job_id) return
|
||||
|
||||
if (updatedJob.status === 'succeeded') {
|
||||
notificationManager.removeNotification(installingNotify.id)
|
||||
notificationManager.addNotification({
|
||||
title: formatMessage(modpackMessages.installed),
|
||||
type: 'success',
|
||||
})
|
||||
unlisten()
|
||||
} else if (['failed', 'canceled', 'interrupted'].includes(updatedJob.status)) {
|
||||
notificationManager.removeNotification(installingNotify.id)
|
||||
unlisten()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleModpackDuplicateCreateAnyway() {
|
||||
if (!pendingModpackCreation.value) return
|
||||
const { projectId, versionId, name, iconUrl } = pendingModpackCreation.value
|
||||
pendingModpackCreation.value = null
|
||||
await proceedWithModpackCreation(projectId, versionId, name, iconUrl)
|
||||
}
|
||||
|
||||
function handleModpackDuplicateGoToInstance(instanceId: string) {
|
||||
pendingModpackCreation.value = null
|
||||
router.push(`/instance/${encodeURIComponent(instanceId)}/`)
|
||||
}
|
||||
|
||||
function handleBrowseModpacks() {
|
||||
installationModal.value?.hide()
|
||||
router.push('/browse/modpack')
|
||||
}
|
||||
|
||||
async function searchModpacks(query: string, limit: number = 10) {
|
||||
const params = [`facets=[["project_type:modpack"]]`, `limit=${limit}`]
|
||||
if (query) {
|
||||
params.push(`query=${encodeURIComponent(query)}`)
|
||||
}
|
||||
const raw = await get_search_results(`?${params.join('&')}`)
|
||||
if (raw?.result) return raw.result
|
||||
return { hits: [], offset: 0, limit, total_hits: 0 }
|
||||
}
|
||||
|
||||
async function getProjectVersions(projectId: string) {
|
||||
const versions = await get_project_versions(projectId, 'must_revalidate')
|
||||
return versions ?? []
|
||||
}
|
||||
|
||||
async function hasCompatibleOptiFabric(gameVersion: string) {
|
||||
const response = await getCurseForgeFiles(OPTIFABRIC_CURSEFORGE_PROJECT_ID, {
|
||||
index: 0,
|
||||
pageSize: 50,
|
||||
})
|
||||
return hasCompatibleCurseForgeFile(response.files, gameVersion)
|
||||
}
|
||||
|
||||
let _currentFlowCtx: CreationFlowContextValue | null = null
|
||||
|
||||
/** Install a modpack file with continuous feedback notifications. */
|
||||
async function installModpackFromPath(
|
||||
filePath: string,
|
||||
fileName: string,
|
||||
options: { persistUntilDone?: boolean } = {},
|
||||
) {
|
||||
const persistUntilDone = options.persistUntilDone === true
|
||||
const currentNotify = notificationManager.addNotification({
|
||||
title: formatMessage(modpackMessages.installingFile, { name: fileName }),
|
||||
type: 'info',
|
||||
autoCloseMs: persistUntilDone ? null : 1000 * 10,
|
||||
})
|
||||
|
||||
const location: CreatePackLocation = { type: 'fromFile', path: filePath }
|
||||
|
||||
try {
|
||||
const isMrpack = fileName?.toLowerCase().endsWith('.mrpack')
|
||||
|
||||
if (!isMrpack) {
|
||||
// .zip needs preview to determine manifest
|
||||
const preview = await install_get_modpack_preview(location).catch((e) => {
|
||||
notificationManager.removeNotification(currentNotify.id)
|
||||
handleError(e)
|
||||
return null
|
||||
})
|
||||
if (!preview) return
|
||||
|
||||
if (preview.unknownFile) {
|
||||
notificationManager.removeNotification(currentNotify.id)
|
||||
unknownPackWarningModal.value?.show(async () => {
|
||||
if (persistUntilDone) {
|
||||
const installingNotify = notificationManager.addNotification({
|
||||
title: formatMessage(modpackMessages.installing),
|
||||
type: 'info',
|
||||
autoCloseMs: null,
|
||||
})
|
||||
await doInstallModpackFile(location, { existingNotify: installingNotify })
|
||||
} else {
|
||||
await doInstallModpackFile(location)
|
||||
}
|
||||
}, fileName)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (persistUntilDone) {
|
||||
await doInstallModpackFile(location, { existingNotify: currentNotify })
|
||||
} else {
|
||||
await doInstallModpackFile(location)
|
||||
}
|
||||
} catch (e) {
|
||||
notificationManager.removeNotification(currentNotify?.id)
|
||||
handleError(e as Error)
|
||||
}
|
||||
}
|
||||
|
||||
provide('setCreationFlowCtx', (ctx: CreationFlowContextValue) => {
|
||||
_currentFlowCtx = ctx
|
||||
})
|
||||
|
||||
return {
|
||||
installationModal,
|
||||
unknownPackWarningModal,
|
||||
fetchExistingInstanceNames,
|
||||
handleCreate,
|
||||
handleBrowseModpacks,
|
||||
searchModpacks,
|
||||
getProjectVersions,
|
||||
hasCompatibleOptiFabric,
|
||||
getLoaderManifest,
|
||||
installModpackFromPath,
|
||||
setModpackAlreadyInstalledModal,
|
||||
handleModpackDuplicateCreateAnyway,
|
||||
handleModpackDuplicateGoToInstance,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user