From 6ef99c59890744ccb0f95ff92434d8cf898e721b Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Tue, 28 Jul 2026 17:36:26 +0100 Subject: [PATCH] feat: profile settings xplat --- apps/app-frontend/src/App.vue | 7 +- .../components/ui/modal/AppSettingsModal.vue | 87 +++- .../ui/settings/account/ProfileSettings.vue | 70 ++++ apps/app-frontend/src/helpers/users.ts | 14 +- apps/app-frontend/src/pages/User.vue | 7 +- .../src/providers/app-settings-modal.ts | 21 + apps/app-frontend/src/providers/setup/auth.ts | 10 +- apps/app/src/api/users.rs | 23 ++ apps/frontend/src/pages/settings/profile.vue | 319 +++------------ .../src/modules/labrinth/users/v2.ts | 32 +- packages/app-lib/src/api/users.rs | 60 ++- packages/app-lib/src/util/fetch.rs | 39 +- .../src/components/base/FloatingActionBar.vue | 74 +++- .../components/base/UnsavedChangesPopup.vue | 14 +- packages/ui/src/components/modal/NewModal.vue | 10 +- .../ui/src/components/modal/TabbedModal.vue | 22 +- .../layouts/shared/user-profile/layout.vue | 6 +- .../wrapped/AccountProfileSettings.vue | 380 ++++++++++++++++++ .../layouts/wrapped/AccountSocialSettings.vue | 5 +- packages/ui/src/layouts/wrapped/index.ts | 1 + .../src/stories/modal/TabbedModal.stories.ts | 42 ++ packages/ui/src/utils/common-messages.ts | 4 +- 22 files changed, 954 insertions(+), 293 deletions(-) create mode 100644 apps/app-frontend/src/components/ui/settings/account/ProfileSettings.vue create mode 100644 apps/app-frontend/src/providers/app-settings-modal.ts create mode 100644 packages/ui/src/layouts/wrapped/AccountProfileSettings.vue diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index ae6ab71590..53a259e3b6 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -152,6 +152,7 @@ import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer' import { get_available_capes, get_available_skins } from './helpers/skins' import { AppNotificationManager } from './providers/app-notifications' import { AppPopupNotificationManager } from './providers/app-popup-notifications' +import { appSettingsModalOpenProfileKey } from './providers/app-settings-modal' const themeStore = useTheming() const router = useRouter() @@ -760,6 +761,8 @@ const sharedInstanceInviteHandler = ref() const updateToPlayModal = ref() const modrinthLoginModal = ref() +const appSettingsModal = ref() +provide(appSettingsModalOpenProfileKey, () => appSettingsModal.value?.showProfile()) watch(incompatibilityWarningModal, (modal) => { if (modal) { @@ -1574,7 +1577,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload) - + @@ -1646,7 +1649,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
diff --git a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue index 4c4ac0710f..76c59c8691 100644 --- a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue +++ b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue @@ -10,6 +10,7 @@ import { Settings2Icon, ShieldIcon, ToggleRightIcon, + UserIcon, } from '@modrinth/assets' import { commonMessages, @@ -18,13 +19,15 @@ import { defineMessages, ProgressBar, TabbedModal, + UnsavedChangesPopup, useVIntl, } from '@modrinth/ui' import { getVersion } from '@tauri-apps/api/app' import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os' -import { computed, ref, watch } from 'vue' +import { computed, provide, ref, watch } from 'vue' import PrivacySettings from '@/components/ui/settings/account/PrivacySettings.vue' +import ProfileSettings from '@/components/ui/settings/account/ProfileSettings.vue' import SocialSettings from '@/components/ui/settings/account/SocialSettings.vue' import AppearanceSettings from '@/components/ui/settings/display/AppearanceSettings.vue' import BehaviorSettings from '@/components/ui/settings/display/BehaviorSettings.vue' @@ -34,6 +37,10 @@ import DefaultInstanceSettings from '@/components/ui/settings/instances/DefaultI import JavaSettings from '@/components/ui/settings/instances/JavaSettings.vue' import ResourceManagementSettings from '@/components/ui/settings/instances/ResourceManagementSettings.vue' import { get, set } from '@/helpers/settings.ts' +import { + appSettingsModalContextKey, + type UnsavedChangesController, +} from '@/providers/app-settings-modal' import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts' import { useTheming } from '@/store/state' @@ -100,6 +107,12 @@ const tabs = [ content: FeatureFlagSettings, developerOnly: true, }, + { + name: commonSettingsMessages.profile, + category: tabCategories.account, + icon: UserIcon, + content: ProfileSettings, + }, { name: commonSettingsMessages.social, category: tabCategories.account, @@ -147,12 +160,62 @@ const tabs = [ const availableTabs = computed(() => tabs.filter((tab) => !tab.developerOnly || themeStore.devMode)) const modal = ref | null>(null) +const unsavedChangesPopup = ref<{ nudge: () => void } | null>(null) +const unsavedChangesController = ref(null) +const emptyUnsavedChangesState: Record = {} +const originalUnsavedChangesState = computed( + () => unsavedChangesController.value?.getOriginal() ?? emptyUnsavedChangesState, +) +const modifiedUnsavedChangesState = computed( + () => unsavedChangesController.value?.getModified() ?? emptyUnsavedChangesState, +) +const savingUnsavedChanges = computed( + () => unsavedChangesController.value?.isSaving() ?? false, +) +const hasUnsavedChanges = computed( + () => unsavedChangesController.value?.hasChanges() ?? false, +) + +function canLeaveCurrentTab(): boolean { + if (!unsavedChangesController.value?.hasChanges()) return true + unsavedChangesPopup.value?.nudge() + return false +} + +function close(): boolean { + return modal.value?.hide() ?? false +} + +function registerUnsavedChangesController(controller: UnsavedChangesController | null): void { + unsavedChangesController.value = controller +} + +provide(appSettingsModalContextKey, { + close, + registerUnsavedChangesController, +}) + +function resetUnsavedChanges(): void { + unsavedChangesController.value?.reset() +} + +function saveUnsavedChanges(): void { + void unsavedChangesController.value?.save() +} function show() { modal.value?.show() } -defineExpose({ show }) +function showProfile(): void { + const profileTabIndex = availableTabs.value.findIndex((tab) => tab.content === ProfileSettings) + if (profileTabIndex >= 0) { + modal.value?.setTab(profileTabIndex) + } + modal.value?.show() +} + +defineExpose({ show, showProfile }) const { progress, version: downloadingVersion } = injectAppUpdateDownloadProgress() @@ -205,12 +268,30 @@ const messages = defineMessages({ })