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 c85852fd5b..2f1f550b66 100644
--- a/apps/app-frontend/src/App.vue
+++ b/apps/app-frontend/src/App.vue
@@ -13,11 +13,10 @@ import {
ChevronLeftIcon,
ChevronRightIcon,
CompassIcon,
- HomeIcon,
- LibraryIcon,
LogInIcon,
LogOutIcon,
NewspaperIcon,
+ 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'
@@ -73,6 +72,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'
@@ -81,6 +81,9 @@ 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 NewIconEditorNotification from '@/components/ui/new-icon-editor-notification/index.vue'
+import { shouldShowNewIconEditorNotification } from '@/components/ui/new-icon-editor-notification/show-notification'
+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'
@@ -314,13 +317,64 @@ const {
fetchExistingInstanceNames,
handleCreate,
handleBrowseModpacks,
- searchModpacks,
- getProjectVersions,
+ searchProjects,
getLoaderManifest,
setModpackAlreadyInstalledModal,
handleModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance,
+ onboardingChecklist,
} = setupProviders(tauriApiClient, notificationManager, popupNotificationManager)
+const { hasLoggedIntoMinecraft, hasLoggedIntoModrinth, showChecklist } = onboardingChecklist
+const showFriendsList = computed(() => !showChecklist.value || hasLoggedIntoModrinth.value)
+const creationIconEditorModal = ref(null)
+const creationIconRecipe = ref(null)
+const creationIconTarget = ref('creation-flow')
+
+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() {
+ creationIconTarget.value = 'creation-flow'
+ creationIconEditorModal.value?.show()
+}
+
+onMounted(async () => {
+ if (!shouldShowNewIconEditorNotification()) return
+
+ addPopupNotification({
+ contentType: 'custom',
+ component: NewIconEditorNotification,
+ autoCloseMs: null,
+ })
+})
+
+function customizeContentInstallIcon() {
+ creationIconTarget.value = 'content-install'
+ creationIconEditorModal.value?.show()
+}
+
+function onCreationIconSaved(iconPath, recipe) {
+ creationIconRecipe.value = recipe
+ if (creationIconTarget.value === 'content-install') {
+ modInstallModal.value?.setIcon(iconPath, convertFileSrc(iconPath))
+ return
+ }
+
+ 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 displayedServerInviteNotifications = new Set()
@@ -336,7 +390,6 @@ window.addEventListener('online', () => {
offline.value = false
})
-const showOnboarding = ref(false)
const nativeDecorations = ref(false)
const os = ref('')
@@ -445,10 +498,6 @@ const messages = defineMessages({
id: 'app.nav.home',
defaultMessage: 'Home',
},
- library: {
- id: 'app.nav.library',
- defaultMessage: 'Library',
- },
modrinthHosting: {
id: 'app.nav.modrinth-hosting',
defaultMessage: 'Modrinth Hosting',
@@ -508,6 +557,7 @@ function handleAdsConsentRequired(required) {
}
const notification = addPopupNotification({
+ contentType: 'standard',
title: formatMessage(messages.adsConsentTitle),
text: formatMessage(messages.adsConsentBody),
type: 'info',
@@ -540,6 +590,8 @@ function handleAdsConsentRequired(required) {
}
async function setupApp() {
+ await onboardingChecklist.initialize()
+
const {
native_decorations,
theme,
@@ -548,8 +600,6 @@ async function setupApp() {
collapsed_navigation,
hide_nametag_skins_page,
advanced_rendering,
- onboarded,
- default_page,
toggle_sidebar,
developer_mode,
feature_flags,
@@ -561,16 +611,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)
@@ -592,7 +636,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())
@@ -804,6 +848,7 @@ const {
projectInfo: contentInstallProjectInfo,
handleInstallToInstance,
handleCreateAndInstall,
+ prepareNewInstance,
handleNavigate: handleContentInstallNavigate,
handleCancel: handleContentInstallCancel,
setContentInstallModal,
@@ -823,6 +868,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 {
@@ -1071,17 +1145,16 @@ async function handleLiveNotification(notification) {
if (generation !== liveNotificationGeneration) return
const popupNotification = addPopupNotification({
+ contentType: 'toast',
title: serverName,
+ type: 'server-invite',
+ actorName: invitedBy?.username ?? null,
+ actorAvatarUrl: invitedBy?.avatar_url ?? null,
+ entityName: serverName,
autoCloseMs: null,
- toast: {
- type: 'server-invite',
- actorName: invitedBy?.username ?? null,
- actorAvatarUrl: invitedBy?.avatar_url ?? null,
- entityName: serverName,
- onAccept: () => acceptServerInviteNotification(notification),
- onDecline: () => declineServerInviteNotification(notification),
- onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null),
- },
+ onAccept: () => acceptServerInviteNotification(notification),
+ onDecline: () => declineServerInviteNotification(notification),
+ onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null),
})
serverInvitePopupNotificationIds.add(popupNotification.id)
}
@@ -1258,6 +1331,7 @@ function showDelayedUpdatePopup() {
if (metered.value && !finishedDownloading.value) {
addPopupNotification({
+ contentType: 'standard',
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.meteredBody, { version: update.version }),
type: 'info',
@@ -1279,6 +1353,7 @@ function showDelayedUpdatePopup() {
})
} else if (finishedDownloading.value) {
addPopupNotification({
+ contentType: 'standard',
title: formatMessage(updatePopupMessages.downloadComplete),
text: formatMessage(updatePopupMessages.downloadedBody, {
version: update.version,
@@ -1377,6 +1452,7 @@ async function checkLinuxUpdates() {
const nextPopupTime = getNextAppUpdatePopupTime(latestVersion)
if (nextPopupTime !== null && Date.now() >= nextPopupTime) {
addPopupNotification({
+ contentType: 'standard',
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.linuxBody, { version: latestVersion }),
type: 'info',
@@ -1559,18 +1635,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"
/>
+