<script lang="ts" setup> import type { mastodon } from 'masto' import { ofetch } from 'ofetch' import { useForm } from 'slimeform' import { parse } from 'ultrahtml' import type { Component } from 'vue' definePageMeta({ middleware: 'auth', }) const { t } = useI18n() useHeadFixed({ title: () => `${t('settings.profile.appearance.title')} | ${t('nav.settings')}`, }) const { client } = $(useMasto()) const avatarInput = ref<any>() const headerInput = ref<any>() const account = $computed(() => currentUser.value?.account) const onlineSrc = $computed(() => ({ avatar: account?.avatar || '', header: account?.header || '', })) const { form, reset, submitter, isDirty, dirtyFields, isError } = useForm({ form: () => { // For complex types of objects, a deep copy is required to ensure correct comparison of initial and modified values const fieldsAttributes = Array.from({ length: maxAccountFieldCount.value }, (_, i) => { const field = { ...account?.fields?.[i] || { name: '', value: '' } } const linkElement = (parse(field.value)?.children?.[0]) if (linkElement && linkElement?.attributes?.href) field.value = linkElement.attributes.href return field }) return { displayName: account?.displayName ?? '', note: account?.source.note.replaceAll('\r', '') ?? '', avatar: null as null | File, header: null as null | File, fieldsAttributes, bot: account?.bot ?? false, // These look more like account and privacy settings than appearance settings // discoverable: false, // locked: false, } }, }) const isCanSubmit = computed(() => !isError.value && isDirty.value) const { submit, submitting } = submitter(async ({ dirtyFields }) => { if (!isCanSubmit.value) return const res = await client.v1.accounts.updateCredentials(dirtyFields.value as mastodon.v1.UpdateCredentialsParams) .then(account => ({ account })) .catch((error: Error) => ({ error })) if ('error' in res) { // TODO: Show error message console.error('Error(updateCredentials):', res.error) return } currentUser.value!.account = res.account reset() }) const refreshInfo = async () => { if (!currentUser.value) return // Keep the information to be edited up to date await refreshAccountInfo() if (!isDirty) reset() } useDropZone(avatarInput, (files) => { if (files?.[0]) form.avatar = files[0] }) useDropZone(headerInput, (files) => { if (files?.[0]) form.header = files[0] }) onHydrated(refreshInfo) onReactivated(refreshInfo) </script> <template> <MainContent back> <template #title> <div text-lg font-bold flex items-center gap-2 @click="$scrollToTop"> <span>{{ $t('settings.profile.appearance.title') }}</span> </div> </template> <form space-y-5 @submit.prevent="submit"> <div v-if="isHydrated && account"> <!-- banner --> <div of-hidden bg="gray-500/20" aspect="3"> <CommonInputImage ref="headerInput" v-model="form.header" :original="onlineSrc.header" w-full h-full /> </div> <CommonCropImage v-model="form.header" :stencil-aspect-ratio="3 / 1" /> <!-- avatar --> <div px-4 flex="~ gap4"> <CommonInputImage ref="avatarInput" v-model="form.avatar" :original="onlineSrc.avatar" mt--10 rounded-full border="bg-base 4" w="sm:30 24" min-w="sm:30 24" h="sm:30 24" /> </div> <CommonCropImage v-model="form.avatar" /> <div px4> <div flex justify-between> <AccountDisplayName :account="{ ...account, displayName: form.displayName }" font-bold sm:text-2xl text-xl /> <label> <AccountBotIndicator show-label px2 py1> <template #prepend> <input v-model="form.bot" type="checkbox" cursor-pointer> </template> </AccountBotIndicator> </label> </div> <AccountHandle :account="account" /> </div> </div> <div px4 py3 space-y-5> <!-- display name --> <label space-y-2 block> <p font-medium> {{ $t('settings.profile.appearance.display_name') }} </p> <input v-model="form.displayName" type="text" input-base> </label> <!-- note --> <label space-y-2 block> <p font-medium> {{ $t('settings.profile.appearance.bio') }} </p> <textarea v-model="form.note" maxlength="500" min-h-10ex input-base /> </label> <!-- metadata --> <SettingsProfileMetadata v-if="isHydrated" v-model:form="form" /> <!-- actions --> <div flex="~ gap2" justify-end> <button type="button" btn-text text-sm flex gap-x-2 items-center text-red @click="reset()" > <div aria-hidden="true" i-ri:eraser-line /> {{ $t('action.reset') }} </button> <button type="submit" btn-solid rounded-full text-sm flex gap-x-2 items-center :disabled="submitting || !isCanSubmit" > <span v-if="submitting" aria-hidden="true" block animate-spin preserve-3d> <span block i-ri:loader-2-fill aria-hidden="true" /> </span> <span v-else aria-hidden="true" block i-ri:save-line /> {{ $t('action.save') }} </button> </div> </div> </form> </MainContent> </template>