diff --git a/Cargo.lock b/Cargo.lock
index 726ebeeed2..d266055679 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11020,6 +11020,7 @@ dependencies = [
"heck 0.5.0",
"hickory-resolver 0.25.2",
"httpdate",
+ "image",
"indicatif",
"itertools 0.14.0",
"modrinth-content-management",
diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json
index 18ff367db0..5a88bd2d92 100644
--- a/apps/app-frontend/package.json
+++ b/apps/app-frontend/package.json
@@ -14,6 +14,7 @@
"test": "vue-tsc --noEmit"
},
"dependencies": {
+ "@dnd-kit/vue": "^0.5.0",
"@modrinth/api-client": "workspace:^",
"@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*",
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
index f7e78a4963..8d781e3aca 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -13,13 +13,12 @@ import {
ChangeSkinIcon,
CompassIcon,
ExternalIcon,
- HomeIcon,
LeftArrowIcon,
- LibraryIcon,
LogInIcon,
LogOutIcon,
NewspaperIcon,
NotepadTextIcon,
+ PlayIcon,
PlusIcon,
RefreshCwIcon,
RightArrowIcon,
@@ -57,7 +56,7 @@ import {
import { renderString } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { getVersion } from '@tauri-apps/api/app'
-import { invoke } from '@tauri-apps/api/core'
+import { convertFileSrc, invoke } from '@tauri-apps/api/core'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
import { openUrl } from '@tauri-apps/plugin-opener'
@@ -75,6 +74,7 @@ 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 UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
+import IconEditorModal from '@/components/ui/instance_settings/icon-editor-modal/index.vue'
import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue'
import MinecraftRequiredModal from '@/components/ui/minecraft-required-modal/MinecraftRequiredModal.vue'
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
@@ -83,6 +83,7 @@ import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyIn
import ModrinthAccountRequiredModal from '@/components/ui/modal/ModrinthAccountRequiredModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import NavButton from '@/components/ui/NavButton.vue'
+import OnboardingChecklist from '@/components/ui/onboarding-checklist/index.vue'
import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
@@ -280,13 +281,42 @@ const {
fetchExistingInstanceNames,
handleCreate,
handleBrowseModpacks,
- searchModpacks,
- getProjectVersions,
+ searchProjects,
getLoaderManifest,
setModpackAlreadyInstalledModal,
handleModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance,
+ onboardingChecklist,
} = setupProviders(notificationManager, popupNotificationManager)
+const { hasLoggedIntoMinecraft, hasLoggedIntoModrinth, showChecklist } = onboardingChecklist
+const showFriendsList = computed(() => !showChecklist.value || hasLoggedIntoModrinth.value)
+const creationIconEditorModal = ref(null)
+const creationIconRecipe = ref(null)
+
+async function randomizeCreationIcon() {
+ const generated = await creationIconEditorModal.value?.randomizeAndSave()
+ if (!generated) return null
+
+ creationIconRecipe.value = generated.recipe
+ return {
+ path: generated.iconPath,
+ previewUrl: convertFileSrc(generated.iconPath),
+ }
+}
+
+function customizeCreationIcon() {
+ creationIconEditorModal.value?.show()
+}
+
+function onCreationIconSaved(iconPath, recipe) {
+ creationIconRecipe.value = recipe
+ const context = installationModal.value?.ctx
+ if (!context) return
+
+ context.instanceIcon.value = null
+ context.instanceIconUrl.value = convertFileSrc(iconPath)
+ context.instanceIconPath.value = iconPath
+}
const news = ref([])
const availableSurvey = ref(false)
@@ -303,7 +333,6 @@ window.addEventListener('online', () => {
offline.value = false
})
-const showOnboarding = ref(false)
const nativeDecorations = ref(false)
const os = ref('')
@@ -454,6 +483,8 @@ function handleAdsConsentRequired(required) {
}
async function setupApp() {
+ await onboardingChecklist.initialize()
+
const {
native_decorations,
theme,
@@ -462,8 +493,6 @@ async function setupApp() {
collapsed_navigation,
hide_nametag_skins_page,
advanced_rendering,
- onboarded,
- default_page,
toggle_sidebar,
developer_mode,
feature_flags,
@@ -475,16 +504,10 @@ async function setupApp() {
i18n.global.locale.value = locale
}
- if (default_page === 'Library') {
- await router.push('/library')
- }
-
os.value = await getOS()
const dev = await isDev()
isDevEnvironment.value = dev
const version = await getVersion()
- showOnboarding.value = !onboarded
-
nativeDecorations.value = native_decorations
if (os.value !== 'MacOS') await getCurrentWindow().setDecorations(native_decorations)
@@ -506,7 +529,7 @@ async function setupApp() {
if (telemetry) {
initAnalytics()
if (dev) debugAnalytics()
- trackEvent('Launched', { version, dev, onboarded })
+ trackEvent('Launched', { version, dev })
}
if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault())
@@ -723,6 +746,7 @@ const {
projectInfo: contentInstallProjectInfo,
handleInstallToInstance,
handleCreateAndInstall,
+ prepareNewInstance,
handleNavigate: handleContentInstallNavigate,
handleCancel: handleContentInstallCancel,
setContentInstallModal,
@@ -742,6 +766,35 @@ const {
handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel,
} = contentInstall
+async function prepareCreationProjectInstall(projectId, projectType) {
+ if (projectType === 'modpack') {
+ await contentInstall.install(
+ projectId,
+ null,
+ null,
+ 'CreationModalProject',
+ undefined,
+ (instanceId) => void router.push(`/instance/${encodeURIComponent(instanceId)}`),
+ )
+ return null
+ }
+
+ await prepareNewInstance(projectId)
+ const info = contentInstallProjectInfo.value
+ if (!info) throw new Error(`Project information is unavailable: '${projectId}'`)
+
+ return {
+ projectId,
+ title: info.title,
+ iconUrl: info.iconUrl,
+ link: info.link,
+ owner: info.owner,
+ compatibleLoaders: [...contentInstallLoaders.value],
+ gameVersions: [...contentInstallGameVersions.value],
+ releaseGameVersions: new Set(contentInstallReleaseGameVersions.value),
+ }
+}
+
const serverInstall = createServerInstall({ router, handleError, popupNotificationManager })
provideServerInstall(serverInstall)
const {
@@ -1585,18 +1638,34 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
type="instance"
show-snapshot-toggle
:fetch-existing-instance-names="fetchExistingInstanceNames"
- :search-modpacks="searchModpacks"
- :get-project-versions="getProjectVersions"
+ :search-projects="searchProjects"
+ :prepare-project-install="prepareCreationProjectInstall"
+ :create-project-install="handleCreateAndInstall"
:get-loader-manifest="getLoaderManifest"
+ :randomize-instance-icon="randomizeCreationIcon"
+ :customize-instance-icon="customizeCreationIcon"
@create="handleCreate"
@browse-modpacks="handleBrowseModpacks"
/>
+
- {{ instance.name }} -
-+ Minecraft was never meant to fit in one jar. +
+- {{ formatMessage(messages.libraryGroupsDescription) }} -
-+ {{ formatMessage(messages.emptyGroup) }} +
++ {{ formatMessage(messages.deleteGroupDescription) }} +
+ + ++ {{ instance.name }} +
++ {{ instance.loader }} {{ instance.game_version }} +
+{{ formatMessage(messages.instancesToDelete) }}
+