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 3db155776b..eeb37273f6 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'
@@ -74,6 +73,7 @@ import FriendsList from '@/components/ui/friends/FriendsList.vue'
import HostingUpdateRequired from '@/components/ui/HostingUpdateRequired.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'
@@ -82,6 +82,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'
@@ -248,7 +251,7 @@ useAppEvent(
'warning',
(event) =>
addNotification({
- title: 'Warning',
+ title: formatMessage(messages.warning),
text: event.message,
type: 'warning',
}),
@@ -332,19 +335,68 @@ provideModalBehavior({
onHide: () => release_ads_window_hold(),
})
+const creationIconEditorModal = ref(null)
+const creationGeneratedIcon = ref(null)
+const creationIconTarget = ref('creation-flow')
+
const {
installationModal,
unknownPackWarningModal,
fetchExistingInstanceNames,
handleCreate,
handleBrowseModpacks,
- searchModpacks,
- getProjectVersions,
+ searchProjects,
getLoaderManifest,
setModpackAlreadyInstalledModal,
handleModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance,
-} = setupProviders(tauriApiClient, notificationManager, popupNotificationManager)
+ onboardingChecklist,
+} = setupProviders(
+ tauriApiClient,
+ notificationManager,
+ popupNotificationManager,
+ appEvents,
+ (iconPath) =>
+ creationGeneratedIcon.value?.path === iconPath ? creationGeneratedIcon.value.config : null,
+)
+const { hasLoggedIntoMinecraft, hasLoggedIntoModrinth, showChecklist } = onboardingChecklist
+const showFriendsList = computed(() => !showChecklist.value || hasLoggedIntoModrinth.value)
+
+async function randomizeCreationIcon() {
+ const generated = await creationIconEditorModal.value?.randomizeAndSave()
+ if (!generated) return null
+
+ creationGeneratedIcon.value = { path: generated.iconPath, config: generated.config }
+ return {
+ path: generated.iconPath,
+ previewUrl: convertFileSrc(generated.iconPath),
+ }
+}
+
+function customizeCreationIcon() {
+ creationIconTarget.value = 'creation-flow'
+ creationIconEditorModal.value?.show()
+}
+
+function customizeContentInstallIcon() {
+ creationIconTarget.value = 'content-install'
+ creationIconEditorModal.value?.show()
+}
+
+function onCreationIconSaved(iconPath, config) {
+ creationGeneratedIcon.value = { path: iconPath, config }
+ 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()
@@ -360,7 +412,6 @@ window.addEventListener('online', () => {
offline.value = false
})
-const showOnboarding = ref(false)
const nativeDecorations = ref(false)
const os = ref('')
@@ -425,6 +476,15 @@ const { formatMessage } = useVIntl()
const formatBytes = useFormatBytes()
const messages = defineMessages({
+ warning: { id: 'app.notification.warning', defaultMessage: 'Warning' },
+ moreOptions: { id: 'app.navigation.more-options', defaultMessage: 'More options' },
+ goBack: { id: 'app.navigation.go-back', defaultMessage: 'Go back' },
+ goForward: { id: 'app.navigation.go-forward', defaultMessage: 'Go forward' },
+ nextImage: { id: 'app.navigation.next-image', defaultMessage: 'Next image' },
+ updateDownloadMissingVersion: {
+ id: 'app.update.download-error.missing-version',
+ defaultMessage: 'Failed to download update: no version available',
+ },
updateInstalledToastTitle: {
id: 'app.update.complete-toast.title',
defaultMessage: 'Version {version} was successfully installed!',
@@ -467,10 +527,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',
@@ -530,6 +586,7 @@ function handleAdsConsentRequired(required) {
}
const notification = addPopupNotification({
+ contentType: 'standard',
title: formatMessage(messages.adsConsentTitle),
text: formatMessage(messages.adsConsentBody),
type: 'info',
@@ -562,6 +619,16 @@ function handleAdsConsentRequired(required) {
}
async function setupApp() {
+ await onboardingChecklist.initialize()
+
+ if (shouldShowNewIconEditorNotification(showChecklist.value)) {
+ addPopupNotification({
+ contentType: 'custom',
+ component: NewIconEditorNotification,
+ autoCloseMs: null,
+ })
+ }
+
const {
native_decorations,
theme,
@@ -570,8 +637,6 @@ async function setupApp() {
collapsed_navigation,
hide_nametag_skins_page,
advanced_rendering,
- onboarded,
- default_page,
toggle_sidebar,
developer_mode,
feature_flags,
@@ -583,16 +648,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)
@@ -614,7 +673,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())
@@ -818,6 +877,7 @@ const {
projectInfo: contentInstallProjectInfo,
handleInstallToInstance,
handleCreateAndInstall,
+ prepareNewInstance,
handleNavigate: handleContentInstallNavigate,
handleCancel: handleContentInstallCancel,
setContentInstallModal,
@@ -837,6 +897,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,
@@ -1090,17 +1179,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)
}
@@ -1277,6 +1365,7 @@ function showDelayedUpdatePopup() {
if (metered.value && !finishedDownloading.value) {
addPopupNotification({
+ contentType: 'standard',
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.meteredBody, { version: update.version }),
type: 'info',
@@ -1298,6 +1387,7 @@ function showDelayedUpdatePopup() {
})
} else if (finishedDownloading.value) {
addPopupNotification({
+ contentType: 'standard',
title: formatMessage(updatePopupMessages.downloadComplete),
text: formatMessage(updatePopupMessages.downloadedBody, {
version: update.version,
@@ -1396,6 +1486,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',
@@ -1415,7 +1506,7 @@ async function downloadAvailableUpdate() {
async function downloadUpdate(versionToDownload) {
if (!versionToDownload) {
- handleError(`Failed to download update: no version available`)
+ handleError(formatMessage(messages.updateDownloadMissingVersion))
return
}
@@ -1579,18 +1670,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"
/>
+
+ {{ formatMessage(messages.welcomeDescription) }} +
++ {{ formatMessage(messages.emptyGroup) }} +
++ {{ formatMessage(messages.deleteGroupDescription) }} +
+ + ++ {{ instance.name }} +
++ {{ instance.loader }} {{ instance.game_version }} +
++ {{ formatMessage(messages.description) }} +
++ {{ + formatMessage(messages.instancesWithoutIcons, { + count: iconlessInstanceIds.length, + }) + }} +
+- {{ formatMessage(messages.signInDescription) }} -
- ++ {{ formatMessage(messages.demoTitle) }} +
++ {{ formatMessage(messages.demoDescription) }} +
+- {{ formatMessage(messages.libraryGroupsDescription) }} -
-