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" /> +
- - + + - - - @@ -1802,9 +1883,17 @@ provideAppUpdateDownloadProgress(appUpdateDownload) :class="{ 'pb-12': !hasPlus }" data-overlayscrollbars-initialize > +