2022-12-26 08:50:11 +00:00
|
|
|
<script lang="ts" setup>
|
2023-01-08 06:21:09 +00:00
|
|
|
import type { mastodon } from 'masto'
|
2023-01-19 16:33:17 +00:00
|
|
|
import { ofetch } from 'ofetch'
|
2022-12-26 08:50:11 +00:00
|
|
|
import { useForm } from 'slimeform'
|
2023-01-02 22:00:23 +00:00
|
|
|
import { parse } from 'ultrahtml'
|
2023-01-19 16:33:17 +00:00
|
|
|
import type { Component } from 'vue'
|
2022-12-26 08:50:11 +00:00
|
|
|
|
|
|
|
definePageMeta({
|
2022-12-29 20:14:05 +00:00
|
|
|
middleware: 'auth',
|
2022-12-26 08:50:11 +00:00
|
|
|
})
|
|
|
|
|
2023-01-04 13:57:12 +00:00
|
|
|
const { t } = useI18n()
|
|
|
|
|
2023-02-18 00:26:28 +00:00
|
|
|
useHydratedHead({
|
2023-01-04 13:57:12 +00:00
|
|
|
title: () => `${t('settings.profile.appearance.title')} | ${t('nav.settings')}`,
|
|
|
|
})
|
|
|
|
|
2023-01-15 08:38:02 +00:00
|
|
|
const { client } = $(useMasto())
|
|
|
|
|
2023-01-19 16:33:17 +00:00
|
|
|
const avatarInput = ref<any>()
|
|
|
|
const headerInput = ref<any>()
|
|
|
|
|
2023-01-02 15:00:11 +00:00
|
|
|
const account = $computed(() => currentUser.value?.account)
|
2022-12-26 08:50:11 +00:00
|
|
|
|
|
|
|
const onlineSrc = $computed(() => ({
|
2023-01-02 15:00:11 +00:00
|
|
|
avatar: account?.avatar || '',
|
|
|
|
header: account?.header || '',
|
2022-12-26 08:50:11 +00:00
|
|
|
}))
|
|
|
|
|
2023-01-19 10:50:56 +00:00
|
|
|
const { form, reset, submitter, isDirty, dirtyFields, isError } = useForm({
|
2023-01-02 15:00:11 +00:00
|
|
|
form: () => {
|
|
|
|
// For complex types of objects, a deep copy is required to ensure correct comparison of initial and modified values
|
2023-01-10 08:33:20 +00:00
|
|
|
const fieldsAttributes = Array.from({ length: maxAccountFieldCount.value }, (_, i) => {
|
2023-01-02 22:00:23 +00:00
|
|
|
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
|
2023-01-02 15:00:11 +00:00
|
|
|
})
|
|
|
|
return {
|
|
|
|
displayName: account?.displayName ?? '',
|
|
|
|
note: account?.source.note.replaceAll('\r', '') ?? '',
|
|
|
|
|
|
|
|
avatar: null as null | File,
|
|
|
|
header: null as null | File,
|
|
|
|
|
|
|
|
fieldsAttributes,
|
|
|
|
|
2023-01-08 14:08:11 +00:00
|
|
|
bot: account?.bot ?? false,
|
|
|
|
|
2023-01-02 15:00:11 +00:00
|
|
|
// These look more like account and privacy settings than appearance settings
|
|
|
|
// discoverable: false,
|
|
|
|
// locked: false,
|
|
|
|
}
|
|
|
|
},
|
2022-12-26 08:50:11 +00:00
|
|
|
})
|
|
|
|
|
2023-01-19 10:50:56 +00:00
|
|
|
const isCanSubmit = computed(() => !isError.value && isDirty.value)
|
2022-12-26 08:50:11 +00:00
|
|
|
|
|
|
|
const { submit, submitting } = submitter(async ({ dirtyFields }) => {
|
2023-01-03 10:13:48 +00:00
|
|
|
if (!isCanSubmit.value)
|
|
|
|
return
|
|
|
|
|
2023-01-15 08:38:02 +00:00
|
|
|
const res = await client.v1.accounts.updateCredentials(dirtyFields.value as mastodon.v1.UpdateCredentialsParams)
|
2022-12-26 08:50:11 +00:00
|
|
|
.then(account => ({ account }))
|
|
|
|
.catch((error: Error) => ({ error }))
|
|
|
|
|
|
|
|
if ('error' in res) {
|
|
|
|
// TODO: Show error message
|
|
|
|
console.error('Error(updateCredentials):', res.error)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-01-15 08:38:02 +00:00
|
|
|
currentUser.value!.account = res.account
|
2022-12-26 08:50:11 +00:00
|
|
|
reset()
|
|
|
|
})
|
2023-01-08 09:06:15 +00:00
|
|
|
|
|
|
|
const refreshInfo = async () => {
|
2023-01-15 08:38:02 +00:00
|
|
|
if (!currentUser.value)
|
|
|
|
return
|
2023-01-08 09:06:15 +00:00
|
|
|
// Keep the information to be edited up to date
|
2023-01-15 08:38:02 +00:00
|
|
|
await refreshAccountInfo()
|
2023-01-08 09:06:15 +00:00
|
|
|
if (!isDirty)
|
|
|
|
reset()
|
|
|
|
}
|
|
|
|
|
2023-01-19 16:33:17 +00:00
|
|
|
useDropZone(avatarInput, (files) => {
|
|
|
|
if (files?.[0])
|
|
|
|
form.avatar = files[0]
|
|
|
|
})
|
|
|
|
useDropZone(headerInput, (files) => {
|
|
|
|
if (files?.[0])
|
|
|
|
form.header = files[0]
|
|
|
|
})
|
|
|
|
|
2023-01-15 08:38:02 +00:00
|
|
|
onHydrated(refreshInfo)
|
2023-01-08 09:06:15 +00:00
|
|
|
onReactivated(refreshInfo)
|
2022-12-26 08:50:11 +00:00
|
|
|
</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">
|
2023-01-06 19:14:00 +00:00
|
|
|
<div v-if="isHydrated && account">
|
2022-12-26 08:50:11 +00:00
|
|
|
<!-- banner -->
|
|
|
|
<div of-hidden bg="gray-500/20" aspect="3">
|
|
|
|
<CommonInputImage
|
2023-01-19 16:33:17 +00:00
|
|
|
ref="headerInput"
|
2022-12-26 08:50:11 +00:00
|
|
|
v-model="form.header"
|
|
|
|
:original="onlineSrc.header"
|
|
|
|
w-full h-full
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<CommonCropImage v-model="form.header" :stencil-aspect-ratio="3 / 1" />
|
|
|
|
|
|
|
|
<!-- avatar -->
|
2023-01-06 19:14:00 +00:00
|
|
|
<div px-4 flex="~ gap4">
|
2022-12-26 08:50:11 +00:00
|
|
|
<CommonInputImage
|
2023-01-19 16:33:17 +00:00
|
|
|
ref="avatarInput"
|
2022-12-26 08:50:11 +00:00
|
|
|
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" />
|
2023-01-08 09:06:15 +00:00
|
|
|
|
|
|
|
<div px4>
|
2023-01-08 14:08:11 +00:00
|
|
|
<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>
|
2023-01-14 10:40:14 +00:00
|
|
|
<input v-model="form.bot" type="checkbox" cursor-pointer>
|
2023-01-08 14:08:11 +00:00
|
|
|
</template>
|
|
|
|
</AccountBotIndicator>
|
|
|
|
</label>
|
|
|
|
</div>
|
2023-01-08 09:06:15 +00:00
|
|
|
<AccountHandle :account="account" />
|
|
|
|
</div>
|
2022-12-26 08:50:11 +00:00
|
|
|
</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>
|
|
|
|
|
2023-01-02 15:00:11 +00:00
|
|
|
<!-- metadata -->
|
|
|
|
|
2023-01-10 08:33:20 +00:00
|
|
|
<SettingsProfileMetadata v-if="isHydrated" v-model:form="form" />
|
2023-01-02 15:00:11 +00:00
|
|
|
|
2023-01-02 21:17:11 +00:00
|
|
|
<!-- 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>
|
|
|
|
|
2022-12-26 08:50:11 +00:00
|
|
|
<button
|
|
|
|
type="submit"
|
|
|
|
btn-solid rounded-full text-sm
|
2023-01-02 21:17:11 +00:00
|
|
|
flex gap-x-2 items-center
|
2022-12-26 08:50:11 +00:00
|
|
|
:disabled="submitting || !isCanSubmit"
|
|
|
|
>
|
2023-01-13 16:00:32 +00:00
|
|
|
<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 />
|
2023-01-02 21:17:11 +00:00
|
|
|
{{ $t('action.save') }}
|
2022-12-26 08:50:11 +00:00
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
</MainContent>
|
|
|
|
</template>
|