mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
feat: profile settings xplat
This commit is contained in:
@@ -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)
|
||||
</div>
|
||||
</Transition>
|
||||
<Suspense>
|
||||
<AppSettingsModal ref="settingsModal" />
|
||||
<AppSettingsModal ref="appSettingsModal" />
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<ModrinthAccountRequiredModal ref="modrinthLoginModal" :request-auth="requestModrinthAuth" />
|
||||
@@ -1646,7 +1649,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<div class="flex flex-grow"></div>
|
||||
<NavButton
|
||||
v-tooltip.right="formatMessage(commonMessages.settingsLabel)"
|
||||
:to="() => $refs.settingsModal.show()"
|
||||
:to="() => appSettingsModal?.show()"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</NavButton>
|
||||
|
||||
@@ -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<InstanceType<typeof TabbedModal> | null>(null)
|
||||
const unsavedChangesPopup = ref<{ nudge: () => void } | null>(null)
|
||||
const unsavedChangesController = ref<UnsavedChangesController | null>(null)
|
||||
const emptyUnsavedChangesState: Record<string, unknown> = {}
|
||||
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({
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<TabbedModal ref="modal" :tabs="availableTabs" :width="'min(928px, calc(95vw - 10rem))'">
|
||||
<TabbedModal
|
||||
ref="modal"
|
||||
:tabs="availableTabs"
|
||||
:width="'min(928px, calc(95vw - 10rem))'"
|
||||
:before-hide="canLeaveCurrentTab"
|
||||
:before-tab-change="canLeaveCurrentTab"
|
||||
:floating-action-bar-shown="hasUnsavedChanges"
|
||||
>
|
||||
<template #title>
|
||||
<span class="text-2xl font-semibold text-contrast">
|
||||
{{ formatMessage(commonMessages.settingsLabel) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #floating-action-bar>
|
||||
<UnsavedChangesPopup
|
||||
ref="unsavedChangesPopup"
|
||||
:original="originalUnsavedChangesState"
|
||||
:modified="modifiedUnsavedChangesState"
|
||||
:saving="savingUnsavedChanges"
|
||||
inline
|
||||
@reset="resetUnsavedChanges"
|
||||
@save="saveUnsavedChanges"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="mt-auto text-secondary text-sm">
|
||||
<div class="mb-3">
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<AccountProfileSettings
|
||||
ref="profileSettings"
|
||||
:patch-user="patchUser"
|
||||
:change-avatar="changeAvatar"
|
||||
:delete-avatar="deleteAvatar"
|
||||
:get-authenticated-user="getAuthenticatedUser"
|
||||
@profile-link-click="handleProfileLinkClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { AccountProfileSettings, injectAuth } from '@modrinth/ui'
|
||||
import { inject, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
change_user_avatar,
|
||||
delete_user_avatar,
|
||||
get_user_profile,
|
||||
patch_user,
|
||||
} from '@/helpers/users'
|
||||
import { appSettingsModalContextKey } from '@/providers/app-settings-modal'
|
||||
|
||||
const settingsModal = inject(appSettingsModalContextKey, null)
|
||||
const auth = injectAuth()
|
||||
const profileSettings = ref<InstanceType<typeof AccountProfileSettings> | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
settingsModal?.registerUnsavedChangesController({
|
||||
hasChanges: () => profileSettings.value?.hasChanges ?? false,
|
||||
getOriginal: () => profileSettings.value?.originalState ?? {},
|
||||
getModified: () => profileSettings.value?.modifiedState ?? {},
|
||||
isSaving: () => profileSettings.value?.saving ?? false,
|
||||
reset: () => profileSettings.value?.reset(),
|
||||
save: () => profileSettings.value?.save(),
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
settingsModal?.registerUnsavedChangesController(null)
|
||||
})
|
||||
|
||||
function handleProfileLinkClick(event: MouseEvent): void {
|
||||
if (settingsModal && !settingsModal.close()) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
function patchUser(
|
||||
userId: string,
|
||||
patch: Partial<Pick<Labrinth.Users.v2.User, 'bio' | 'username'>>,
|
||||
): Promise<void> {
|
||||
return patch_user(userId, patch)
|
||||
}
|
||||
|
||||
async function changeAvatar(userId: string, file: Blob, extension: string): Promise<void> {
|
||||
await change_user_avatar(userId, new Uint8Array(await file.arrayBuffer()), extension)
|
||||
}
|
||||
|
||||
function deleteAvatar(userId: string): Promise<void> {
|
||||
return delete_user_avatar(userId)
|
||||
}
|
||||
|
||||
function getAuthenticatedUser(): Promise<Labrinth.Users.v3.User> {
|
||||
const userId = auth.user.value?.id
|
||||
if (!userId) throw new Error('Cannot refresh a signed-out user.')
|
||||
return get_user_profile(userId)
|
||||
}
|
||||
</script>
|
||||
@@ -50,11 +50,23 @@ export async function get_user_collections(
|
||||
|
||||
export async function patch_user(
|
||||
userId: string,
|
||||
patch: Partial<Pick<Labrinth.Users.v3.User, 'badges' | 'role'>>,
|
||||
patch: Partial<Pick<Labrinth.Users.v2.User, 'badges' | 'bio' | 'role' | 'username'>>,
|
||||
): Promise<void> {
|
||||
await invoke('plugin:users|patch_user', { userId, patch })
|
||||
}
|
||||
|
||||
export async function change_user_avatar(
|
||||
userId: string,
|
||||
image: Uint8Array,
|
||||
extension: string,
|
||||
): Promise<void> {
|
||||
await invoke('plugin:users|change_user_avatar', { userId, image, extension })
|
||||
}
|
||||
|
||||
export async function delete_user_avatar(userId: string): Promise<void> {
|
||||
await invoke('plugin:users|delete_user_avatar', { userId })
|
||||
}
|
||||
|
||||
export async function block_user(userId: string): Promise<void> {
|
||||
await invoke('plugin:users|block_user', { userId })
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<div class="w-full pt-2">
|
||||
<div class="w-full px-2 pt-2">
|
||||
<UserProfilePageLayout
|
||||
:user-id="userId"
|
||||
:project-type="projectType"
|
||||
variant="app"
|
||||
site-url="https://modrinth.com"
|
||||
project-link-mode="app"
|
||||
:edit-profile-link="openProfileSettings"
|
||||
external-navigation
|
||||
/>
|
||||
</div>
|
||||
@@ -14,7 +15,7 @@
|
||||
<script setup lang="ts">
|
||||
import { provideUserProfile, UserProfilePageLayout } from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, watch } from 'vue'
|
||||
import { computed, inject, watch } from 'vue'
|
||||
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
|
||||
|
||||
import {
|
||||
@@ -27,9 +28,11 @@ import {
|
||||
patch_user,
|
||||
unblock_user,
|
||||
} from '@/helpers/users'
|
||||
import { appSettingsModalOpenProfileKey } from '@/providers/app-settings-modal'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
|
||||
const route = useRoute()
|
||||
const openProfileSettings = inject(appSettingsModalOpenProfileKey, () => {})
|
||||
const queryClient = useQueryClient()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const userProfile = provideUserProfile({
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { InjectionKey } from 'vue'
|
||||
|
||||
export type UnsavedChangesController = {
|
||||
hasChanges: () => boolean
|
||||
getOriginal: () => Record<string, unknown>
|
||||
getModified: () => Record<string, unknown>
|
||||
isSaving: () => boolean
|
||||
reset: () => void
|
||||
save: () => void | Promise<void>
|
||||
}
|
||||
|
||||
export type AppSettingsModalContext = {
|
||||
close: () => boolean
|
||||
registerUnsavedChangesController: (controller: UnsavedChangesController | null) => void
|
||||
}
|
||||
|
||||
export const appSettingsModalContextKey: InjectionKey<AppSettingsModalContext> =
|
||||
Symbol('appSettingsModalContext')
|
||||
export const appSettingsModalOpenProfileKey: InjectionKey<() => void> = Symbol(
|
||||
'appSettingsModalOpenProfile',
|
||||
)
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type AuthUser,
|
||||
provideAuth,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, type Ref, ref, watchEffect } from 'vue'
|
||||
import { computed, type Ref, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
type AppCredentials = {
|
||||
session?: string | null
|
||||
@@ -37,5 +37,13 @@ export function setupAuthProvider(
|
||||
user.value = credentials.value?.user ?? null
|
||||
})
|
||||
|
||||
watch(user, (updatedUser) => {
|
||||
if (!credentials.value || !updatedUser || credentials.value.user === updatedUser) return
|
||||
credentials.value = {
|
||||
...credentials.value,
|
||||
user: updatedUser,
|
||||
}
|
||||
})
|
||||
|
||||
provideAuth(authProvider)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,27 @@ pub async fn patch_user(user_id: &str, patch: Value) -> Result<()> {
|
||||
Ok(theseus::users::patch_user(user_id, patch).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn change_user_avatar(
|
||||
user_id: &str,
|
||||
image: Vec<u8>,
|
||||
extension: &str,
|
||||
) -> Result<()> {
|
||||
Ok(
|
||||
theseus::users::change_user_avatar(
|
||||
user_id,
|
||||
image.into(),
|
||||
extension,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_user_avatar(user_id: &str) -> Result<()> {
|
||||
Ok(theseus::users::delete_user_avatar(user_id).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn block_user(user_id: &str) -> Result<()> {
|
||||
Ok(theseus::users::block_user(user_id).await?)
|
||||
@@ -56,6 +77,8 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
get_user_organizations,
|
||||
get_user_collections,
|
||||
patch_user,
|
||||
change_user_avatar,
|
||||
delete_user_avatar,
|
||||
block_user,
|
||||
unblock_user,
|
||||
get_blocked_users,
|
||||
|
||||
@@ -1,276 +1,77 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="card">
|
||||
<h2 class="text-2xl">{{ formatMessage(messages.title) }}</h2>
|
||||
<p class="mb-4">
|
||||
<IntlFormatted :message-id="messages.description">
|
||||
<template #docs-link="{ children }">
|
||||
<a href="https://docs.modrinth.com/" target="_blank" class="text-link">
|
||||
<component :is="() => children" />
|
||||
</a>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
<label>
|
||||
<span class="label__title">{{ formatMessage(messages.profilePicture) }}</span>
|
||||
</label>
|
||||
<div class="avatar-changer">
|
||||
<Avatar
|
||||
:src="previewImage ? previewImage : avatarUrl"
|
||||
size="md"
|
||||
circle
|
||||
:alt="auth.user.username"
|
||||
/>
|
||||
<div class="flex flex-col gap-2">
|
||||
<ButtonStyled>
|
||||
<FileInput
|
||||
:max-size="262144"
|
||||
:show-icon="true"
|
||||
class="button-like"
|
||||
:prompt="formatMessage(commonMessages.uploadImageButton)"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
@change="showPreviewImage"
|
||||
>
|
||||
<UploadIcon />
|
||||
</FileInput>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="avatarUrl !== null">
|
||||
<button @click="removePreviewImage">
|
||||
<TrashIcon />
|
||||
{{ formatMessage(commonMessages.removeImageButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="previewImage">
|
||||
<button
|
||||
@click="
|
||||
() => {
|
||||
icon = null
|
||||
previewImage = null
|
||||
}
|
||||
"
|
||||
>
|
||||
<UndoIcon />
|
||||
{{ formatMessage(commonMessages.resetButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<label for="username-field">
|
||||
<span class="label__title">{{ formatMessage(commonMessages.usernameLabel) }}</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(messages.usernameDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<StyledInput id="username-field" v-model="current.username" />
|
||||
<div
|
||||
v-if="current.username.length >= 30"
|
||||
id="bio-character-limit"
|
||||
class="inline-block pl-2"
|
||||
:class="{ 'text-red': current.username.length > 39 }"
|
||||
>
|
||||
{{ current.username.length }}/{{ 39 }}
|
||||
</div>
|
||||
<label for="bio-field">
|
||||
<span class="label__title">{{ formatMessage(messages.bioTitle) }}</span>
|
||||
<span class="label__description">
|
||||
{{ formatMessage(messages.bioDescription) }}
|
||||
</span>
|
||||
</label>
|
||||
<StyledInput id="bio-field" v-model="current.bio" multiline />
|
||||
<div id="bio-character-limit" class="pt-2" :class="{ 'text-red': current.bio.length > 160 }">
|
||||
{{ current.bio.length }}/{{ 160 }}
|
||||
</div>
|
||||
<div class="input-group mt-4">
|
||||
<ButtonStyled>
|
||||
<NuxtLink :to="`/user/${auth.user.username}`">
|
||||
<UserIcon /> {{ formatMessage(commonMessages.visitYourProfile) }}
|
||||
</NuxtLink>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</section>
|
||||
<UnsavedChangesPopup
|
||||
:original="originalState"
|
||||
:modified="modifiedState"
|
||||
:saving="saving"
|
||||
@reset="reset"
|
||||
@save="save"
|
||||
<section v-if="auth.user" class="universal-card">
|
||||
<AccountProfileSettings
|
||||
ref="profileSettings"
|
||||
:patch-user="patchUser"
|
||||
:change-avatar="changeAvatar"
|
||||
:delete-avatar="deleteAvatar"
|
||||
:get-authenticated-user="getAuthenticatedUser"
|
||||
disclaimer-position="bottom"
|
||||
/>
|
||||
</div>
|
||||
<UnsavedChangesPopup
|
||||
:original="profileSettings?.originalState ?? emptyProfileState"
|
||||
:modified="profileSettings?.modifiedState ?? emptyProfileState"
|
||||
:saving="profileSettings?.saving ?? false"
|
||||
@reset="resetProfileSettings"
|
||||
@save="saveProfileSettings"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { TrashIcon, UndoIcon, UploadIcon, UserIcon } from '@modrinth/assets'
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
FileInput,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
StyledInput,
|
||||
AccountProfileSettings,
|
||||
commonSettingsMessages,
|
||||
injectModrinthClient,
|
||||
UnsavedChangesPopup,
|
||||
useSavable,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
headTitle: {
|
||||
id: 'settings.profile.head-title',
|
||||
defaultMessage: 'Profile settings',
|
||||
},
|
||||
title: {
|
||||
id: 'settings.profile.profile-info',
|
||||
defaultMessage: 'Profile information',
|
||||
},
|
||||
description: {
|
||||
id: 'settings.profile.description',
|
||||
defaultMessage:
|
||||
'Your profile information is publicly viewable on Modrinth and through the <docs-link>Modrinth API</docs-link>.',
|
||||
},
|
||||
profilePicture: {
|
||||
id: 'settings.profile.profile-picture.title',
|
||||
defaultMessage: 'Profile picture',
|
||||
},
|
||||
usernameDescription: {
|
||||
id: 'settings.profile.username.description',
|
||||
defaultMessage: 'A unique case-insensitive name to identify your profile.',
|
||||
},
|
||||
bioTitle: {
|
||||
id: 'settings.profile.bio.title',
|
||||
defaultMessage: 'Bio',
|
||||
},
|
||||
bioDescription: {
|
||||
id: 'settings.profile.bio.description',
|
||||
defaultMessage: 'A short description to tell everyone a little bit about you.',
|
||||
},
|
||||
})
|
||||
const auth = await useAuth()
|
||||
const client = injectModrinthClient()
|
||||
const { formatMessage } = useVIntl()
|
||||
const profileSettings = ref<InstanceType<typeof AccountProfileSettings> | null>(null)
|
||||
const emptyProfileState = {
|
||||
username: '',
|
||||
bio: '',
|
||||
avatarChanged: false,
|
||||
}
|
||||
|
||||
function patchUser(
|
||||
userId: string,
|
||||
patch: Partial<Pick<Labrinth.Users.v2.User, 'bio' | 'username'>>,
|
||||
): Promise<void> {
|
||||
return client.labrinth.users_v2.patch(userId, patch)
|
||||
}
|
||||
|
||||
function changeAvatar(userId: string, file: Blob, extension: string): Promise<void> {
|
||||
return client.labrinth.users_v2.changeIcon(userId, file, extension)
|
||||
}
|
||||
|
||||
function deleteAvatar(userId: string): Promise<void> {
|
||||
return client.labrinth.users_v2.deleteIcon(userId)
|
||||
}
|
||||
|
||||
async function getAuthenticatedUser(): Promise<Labrinth.Users.v3.User> {
|
||||
const user = await client.labrinth.users_v3.getAuthenticated()
|
||||
auth.value.user = user
|
||||
return user
|
||||
}
|
||||
|
||||
function resetProfileSettings(): void {
|
||||
profileSettings.value?.reset()
|
||||
}
|
||||
|
||||
function saveProfileSettings(): void {
|
||||
void profileSettings.value?.save()
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: () => `${formatMessage(messages.headTitle)} - Modrinth`,
|
||||
title: () => `${formatMessage(commonSettingsMessages.profile)} - Modrinth`,
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
|
||||
// Avatar state (separate from useSavable)
|
||||
const avatarUrl = ref(auth.value.user.avatar_url)
|
||||
const icon = shallowRef(null)
|
||||
const previewImage = shallowRef(null)
|
||||
const pendingAvatarDeletion = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
const {
|
||||
saved,
|
||||
current,
|
||||
reset: resetFields,
|
||||
} = useSavable(
|
||||
() => ({
|
||||
username: auth.value.user.username,
|
||||
bio: auth.value.user.bio ?? '',
|
||||
}),
|
||||
async () => {}, // Save is handled manually due to complex icon logic
|
||||
)
|
||||
|
||||
// Combined state for UnsavedChangesPopup
|
||||
const originalState = computed(() => ({
|
||||
...saved.value,
|
||||
avatarChanged: false,
|
||||
}))
|
||||
|
||||
const modifiedState = computed(() => ({
|
||||
...current.value,
|
||||
avatarChanged: !!(previewImage.value || pendingAvatarDeletion.value),
|
||||
}))
|
||||
|
||||
const reset = () => {
|
||||
resetFields()
|
||||
icon.value = null
|
||||
previewImage.value = null
|
||||
pendingAvatarDeletion.value = false
|
||||
}
|
||||
|
||||
function showPreviewImage(files) {
|
||||
const reader = new FileReader()
|
||||
icon.value = files[0]
|
||||
reader.readAsDataURL(icon.value)
|
||||
reader.onload = (event) => {
|
||||
previewImage.value = event.target.result
|
||||
}
|
||||
}
|
||||
|
||||
function removePreviewImage() {
|
||||
pendingAvatarDeletion.value = true
|
||||
previewImage.value = 'https://cdn.modrinth.com/placeholder.png'
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
if (pendingAvatarDeletion.value) {
|
||||
await useBaseFetch(`user/${auth.value.user.id}/icon`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
pendingAvatarDeletion.value = false
|
||||
previewImage.value = null
|
||||
}
|
||||
|
||||
if (icon.value) {
|
||||
await useBaseFetch(
|
||||
`user/${auth.value.user.id}/icon?ext=${
|
||||
icon.value.type.split('/')[icon.value.type.split('/').length - 1]
|
||||
}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: icon.value,
|
||||
},
|
||||
)
|
||||
icon.value = null
|
||||
previewImage.value = null
|
||||
}
|
||||
|
||||
const body = {}
|
||||
|
||||
if (auth.value.user.username !== current.value.username) {
|
||||
body.username = current.value.username
|
||||
}
|
||||
|
||||
if (auth.value.user.bio !== current.value.bio) {
|
||||
body.bio = current.value.bio
|
||||
}
|
||||
|
||||
await useBaseFetch(`user/${auth.value.user.id}`, {
|
||||
method: 'PATCH',
|
||||
body,
|
||||
})
|
||||
await useAuth(auth.value.token)
|
||||
avatarUrl.value = auth.value.user.avatar_url
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: err
|
||||
? err.data
|
||||
? err.data.description
|
||||
? err.data.description
|
||||
: err.data
|
||||
: err
|
||||
: 'aaaaahhh',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
saving.value = false
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.avatar-changer {
|
||||
display: flex;
|
||||
gap: var(--gap-lg);
|
||||
margin-top: var(--gap-md);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user