diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 18a686332b..fba749fb6f 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -36,6 +36,7 @@ import { ButtonStyled, commonMessages, ContentInstallModal, + ContentUpdaterModal, CreationFlowModal, defineMessages, I18nDebugPanel, @@ -75,7 +76,6 @@ import Breadcrumbs from '@/components/ui/Breadcrumbs.vue' import ErrorModal from '@/components/ui/ErrorModal.vue' import FriendsList from '@/components/ui/friends/FriendsList.vue' import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue' -import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue' import UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue' import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue' import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue' @@ -611,6 +611,16 @@ const { handleModpackDuplicateCreateAnyway: handleContentInstallModpackDuplicateCreateAnyway, handleModpackDuplicateGoToInstance: handleContentInstallModpackDuplicateGoToInstance, setIncompatibilityWarningModal: setContentIncompatibilityWarningModal, + incompatibilityWarningVersions: contentInstallIncompatibilityWarningVersions, + incompatibilityWarningCurrentGameVersion: contentInstallIncompatibilityWarningCurrentGameVersion, + incompatibilityWarningCurrentLoader: contentInstallIncompatibilityWarningCurrentLoader, + incompatibilityWarningProjectType: contentInstallIncompatibilityWarningProjectType, + incompatibilityWarningProjectIconUrl: contentInstallIncompatibilityWarningProjectIconUrl, + incompatibilityWarningProjectName: contentInstallIncompatibilityWarningProjectName, + incompatibilityWarningMessage: contentInstallIncompatibilityWarningMessage, + incompatibilityWarningInstalling: contentInstallIncompatibilityWarningInstalling, + handleIncompatibilityWarningInstall: handleContentInstallIncompatibilityWarningInstall, + handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel, } = contentInstall const serverInstall = createServerInstall({ router, handleError, popupNotificationManager }) @@ -632,6 +642,12 @@ const updateToPlayModal = ref() const modrinthLoginFlowWaitModal = ref() +watch(incompatibilityWarningModal, (modal) => { + if (modal) { + setContentIncompatibilityWarningModal(modal) + } +}) + setupAuthProvider(credentials, async (_redirectPath) => { await signIn() }) @@ -1550,7 +1566,22 @@ provideAppUpdateDownloadProgress(appUpdateDownload) @go-to-instance="handleModpackDuplicateGoToInstance" /> - + - - - - - - - - diff --git a/apps/app-frontend/src/providers/content-install.ts b/apps/app-frontend/src/providers/content-install.ts index ac36c29d9f..30067ec4ca 100644 --- a/apps/app-frontend/src/providers/content-install.ts +++ b/apps/app-frontend/src/providers/content-install.ts @@ -34,7 +34,7 @@ import { } from '@/store/install.js' interface ModalRef { - show: () => void + show: (initialVersionId?: string) => void hide: () => void } @@ -42,16 +42,6 @@ interface ModpackAlreadyInstalledModalRef { show: (instanceName: string, instancePath: string) => 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 = new Set(['vanilla', 'forge', 'fabric', 'quilt', 'neoforge']) const VANILLA_COMPATIBLE_LOADERS: Set = new Set(['minecraft', 'datapack']) @@ -91,7 +81,17 @@ export interface ContentInstallContext { setModpackAlreadyInstalledModal: (ref: ModpackAlreadyInstalledModalRef) => void handleModpackDuplicateCreateAnyway: () => Promise handleModpackDuplicateGoToInstance: (instancePath: string) => void - setIncompatibilityWarningModal: (ref: IncompatibilityWarningModalRef) => void + setIncompatibilityWarningModal: (ref: ModalRef) => void + incompatibilityWarningVersions: Ref + incompatibilityWarningCurrentGameVersion: Ref + incompatibilityWarningCurrentLoader: Ref + incompatibilityWarningProjectType: Ref + incompatibilityWarningProjectIconUrl: Ref + incompatibilityWarningProjectName: Ref + incompatibilityWarningMessage: Ref + incompatibilityWarningInstalling: Ref + handleIncompatibilityWarningInstall: (version: Labrinth.Versions.v2.Version) => Promise + handleIncompatibilityWarningCancel: () => void install: ( projectId: string, versionId?: string | null, @@ -124,6 +124,14 @@ export function createContentInstall(opts: { const projectInfo = ref(null) const installingItems = ref>(new Map()) + const incompatibilityWarningVersions = ref([]) + const incompatibilityWarningCurrentGameVersion = ref('') + const incompatibilityWarningCurrentLoader = ref('') + const incompatibilityWarningProjectType = ref(undefined) + const incompatibilityWarningProjectIconUrl = ref(undefined) + const incompatibilityWarningProjectName = ref(undefined) + const incompatibilityWarningMessage = ref(undefined) + const incompatibilityWarningInstalling = ref(false) function addInstallingItem( instancePath: string, @@ -239,11 +247,15 @@ export function createContentInstall(opts: { let modalRef: ModalRef | null = null let modpackAlreadyInstalledModalRef: ModpackAlreadyInstalledModalRef | null = null - let incompatibilityWarningModalRef: IncompatibilityWarningModalRef | null = null + let incompatibilityWarningModalRef: ModalRef | null = null let currentProject: Labrinth.Projects.v2.Project | null = null let currentVersions: Labrinth.Versions.v2.Version[] = [] let currentCallback: (versionId?: string) => void = () => {} let profileMap: Record = {} + let incompatibilityWarningInstance: GameInstance | null = null + let incompatibilityWarningProject: Labrinth.Projects.v2.Project | null = null + let incompatibilityWarningCallback: (versionId?: string) => void = () => {} + let incompatibilityWarningInstalled = false let pendingModpackInstall: { project: Labrinth.Projects.v2.Project @@ -410,15 +422,35 @@ export function createContentInstall(opts: { 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 + if (!currentProject || !profile) { + opts.handleError('No project or instance found') + return + } const version = findPreferredVersion(currentVersions, currentProject, profile) if (!version) { - if (storeInstance) storeInstance.installing = false - opts.handleError('No compatible version found') + if (currentVersions.length > 0 && incompatibilityWarningModalRef) { + const onIncompatibleInstall = (versionId?: string) => { + if (versionId && storeInstance) { + storeInstance.installed = true + } + currentCallback(versionId) + } + await showIncompatibilityWarning( + profile, + currentProject, + currentVersions, + currentVersions[0], + onIncompatibleInstall, + ) + } else { + opts.handleError('No version found') + } return } + if (storeInstance) storeInstance.installing = true + const installedProjectIds: string[] = [] if (currentProject) { addInstallingItem(instance.id, currentProject, version) @@ -458,6 +490,71 @@ export function createContentInstall(opts: { } } + async function showIncompatibilityWarning( + instance: GameInstance, + project: Labrinth.Projects.v2.Project, + versions: Labrinth.Versions.v2.Version[], + version: Labrinth.Versions.v2.Version, + callback: (versionId?: string) => void, + ) { + incompatibilityWarningInstance = instance + incompatibilityWarningProject = project + incompatibilityWarningCallback = callback + incompatibilityWarningInstalled = false + incompatibilityWarningInstalling.value = false + incompatibilityWarningVersions.value = versions + incompatibilityWarningCurrentGameVersion.value = instance.game_version ?? '' + incompatibilityWarningCurrentLoader.value = instance.loader ?? '' + incompatibilityWarningProjectType.value = project.project_type + incompatibilityWarningProjectIconUrl.value = project.icon_url ?? undefined + incompatibilityWarningProjectName.value = project.title + + const compatibilityLabel = + project.project_type === 'resourcepack' || project.project_type === 'datapack' + ? (instance.game_version ?? '') + : `${instance.loader ?? ''} ${instance.game_version ?? ''}`.trim() + incompatibilityWarningMessage.value = `No available versions match ${compatibilityLabel}. Select a version to install anyway. Dependencies will not be installed automatically.` + + await nextTick() + incompatibilityWarningModalRef?.show(version.id) + trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' }) + } + + async function handleIncompatibilityWarningInstall(version: Labrinth.Versions.v2.Version) { + if (!incompatibilityWarningInstance || !incompatibilityWarningProject) return + + incompatibilityWarningInstalling.value = true + try { + await add_project_from_version(incompatibilityWarningInstance.path, version.id, 'standalone') + } catch (err) { + opts.handleError(err) + incompatibilityWarningInstalling.value = false + return + } + + incompatibilityWarningInstalling.value = false + incompatibilityWarningInstalled = true + incompatibilityWarningCallback(version.id) + incompatibilityWarningModalRef?.hide() + + trackEvent('ProjectInstall', { + loader: incompatibilityWarningInstance.loader, + game_version: incompatibilityWarningInstance.game_version, + id: incompatibilityWarningProject.id, + version_id: version.id, + project_type: incompatibilityWarningProject.project_type, + title: incompatibilityWarningProject.title, + source: 'ProjectIncompatibilityWarningModal', + }) + } + + function handleIncompatibilityWarningCancel() { + if (!incompatibilityWarningInstalled) { + incompatibilityWarningCallback() + } + incompatibilityWarningInstalled = false + } + async function handleCreateAndInstall(data: { name: string iconPath: string | null @@ -614,7 +711,7 @@ export function createContentInstall(opts: { removeInstallingItems(instancePath, installedProjectIds) } } else { - incompatibilityWarningModalRef?.show(instance, project, projectVersions, version, callback) + await showIncompatibilityWarning(instance, project, projectVersions, version, callback) } } else { let versions = ( @@ -668,9 +765,19 @@ export function createContentInstall(opts: { pendingModpackInstall = null opts.router.push(`/instance/${encodeURIComponent(instancePath)}`) }, - setIncompatibilityWarningModal(ref: IncompatibilityWarningModalRef) { + setIncompatibilityWarningModal(ref: ModalRef) { incompatibilityWarningModalRef = ref }, + incompatibilityWarningVersions, + incompatibilityWarningCurrentGameVersion, + incompatibilityWarningCurrentLoader, + incompatibilityWarningProjectType, + incompatibilityWarningProjectIconUrl, + incompatibilityWarningProjectName, + incompatibilityWarningMessage, + incompatibilityWarningInstalling, + handleIncompatibilityWarningInstall, + handleIncompatibilityWarningCancel, install, installingItems, } diff --git a/packages/ui/src/components/base/StyledInput.vue b/packages/ui/src/components/base/StyledInput.vue index 2c0e2f00a5..bb3503fcb5 100644 --- a/packages/ui/src/components/base/StyledInput.vue +++ b/packages/ui/src/components/base/StyledInput.vue @@ -71,9 +71,6 @@ variant === 'outlined' ? 'bg-transparent border border-solid border-button-bg rounded-l-xl border-r-0' : 'bg-surface-4 border-none rounded-xl', - { - 'placeholder:text-sm': type === 'search', - }, ]" @input="onInput" @focus="isFocused = true" diff --git a/packages/ui/src/components/search/SearchSidebarFilter.vue b/packages/ui/src/components/search/SearchSidebarFilter.vue index 2100d61643..4e644365cc 100644 --- a/packages/ui/src/components/search/SearchSidebarFilter.vue +++ b/packages/ui/src/components/search/SearchSidebarFilter.vue @@ -5,7 +5,7 @@ :button-class="buttonClass ?? 'flex flex-col gap-2 justify-start items-start'" :content-class="contentClass" title-wrapper-class="flex flex-col gap-2 justify-start items-start" - :open-by-default="!locked && (openByDefault !== undefined ? openByDefault : true)" + :open-by-default="openByDefault !== undefined ? openByDefault : true" >