refactor: migrate to nuxt compatibilityVersion: 4 (#3298)
This commit is contained in:
parent
46e4433e1c
commit
a3fbc056a9
342 changed files with 1200 additions and 2932 deletions
252
app/pages/settings/profile/appearance.vue
Normal file
252
app/pages/settings/profile/appearance.vue
Normal file
|
@ -0,0 +1,252 @@
|
|||
<script setup lang="ts">
|
||||
import type { mastodon } from 'masto'
|
||||
import { useForm } from 'slimeform'
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
useHydratedHead({
|
||||
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.value?.avatar || '',
|
||||
header: account.value?.header || '',
|
||||
}))
|
||||
|
||||
const { form, reset, submitter, isDirty, 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.value?.fields?.[i] || { name: '', value: '' } }
|
||||
|
||||
field.value = convertMetadata(field.value)
|
||||
|
||||
return field
|
||||
})
|
||||
return {
|
||||
displayName: account.value?.displayName ?? '',
|
||||
note: account.value?.source.note.replaceAll('\r', '') ?? '',
|
||||
|
||||
avatar: null as null | File,
|
||||
header: null as null | File,
|
||||
|
||||
fieldsAttributes,
|
||||
|
||||
bot: account.value?.bot ?? false,
|
||||
locked: account.value?.locked ?? false,
|
||||
|
||||
// These look more like account and privacy settings than appearance settings
|
||||
// discoverable: false,
|
||||
// locked: false,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const isCanSubmit = computed(() => !isError.value && isDirty.value)
|
||||
const failedMessages = ref<string[]>([])
|
||||
|
||||
const { submit, submitting } = submitter(async ({ dirtyFields }) => {
|
||||
if (!isCanSubmit.value)
|
||||
return
|
||||
|
||||
const res = await client.value.v1.accounts.updateCredentials(dirtyFields.value as mastodon.rest.v1.UpdateCredentialsParams)
|
||||
.then(account => ({ account }))
|
||||
.catch((error: Error) => ({ error }))
|
||||
|
||||
if ('error' in res) {
|
||||
console.error(res.error)
|
||||
failedMessages.value.push(res.error.message)
|
||||
return
|
||||
}
|
||||
|
||||
const server = currentUser.value!.server
|
||||
|
||||
if (!res.account.acct.includes('@'))
|
||||
res.account.acct = `${res.account.acct}@${server}`
|
||||
|
||||
cacheAccount(res.account, server, true)
|
||||
currentUser.value!.account = res.account
|
||||
reset()
|
||||
})
|
||||
|
||||
async function refreshInfo() {
|
||||
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="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
|
||||
/>
|
||||
<div flex="~ row" items-center gap2>
|
||||
<label>
|
||||
<AccountLockIndicator show-label px2 py1>
|
||||
<template #prepend>
|
||||
<input v-model="form.locked" type="checkbox" cursor-pointer>
|
||||
</template>
|
||||
</AccountLockIndicator>
|
||||
</label>
|
||||
<label>
|
||||
<AccountBotIndicator show-label px2 py1>
|
||||
<template #prepend>
|
||||
<input v-model="form.bot" type="checkbox" cursor-pointer>
|
||||
</template>
|
||||
</AccountBotIndicator>
|
||||
</label>
|
||||
</div>
|
||||
</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-model="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
|
||||
v-if="failedMessages.length === 0"
|
||||
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>
|
||||
|
||||
<button
|
||||
v-else
|
||||
type="submit"
|
||||
btn-danger rounded-full text-sm
|
||||
flex gap-x-2 items-center
|
||||
>
|
||||
<span
|
||||
aria-hidden="true" block i-carbon:face-dizzy-filled
|
||||
/>
|
||||
<span>{{ $t('state.save_failed') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CommonErrorMessage v-if="failedMessages.length > 0" described-by="save-failed">
|
||||
<header id="save-failed" flex justify-between>
|
||||
<div flex items-center gap-x-2 font-bold>
|
||||
<div aria-hidden="true" i-ri:error-warning-fill />
|
||||
<p>{{ $t('state.save_failed') }}</p>
|
||||
</div>
|
||||
<CommonTooltip placement="bottom" :content="$t('action.clear_save_failed')">
|
||||
<button
|
||||
flex rounded-4 p1 hover:bg-active cursor-pointer transition-100 :aria-label="$t('action.clear_save_failed')"
|
||||
@click="failedMessages = []"
|
||||
>
|
||||
<span aria-hidden="true" w="1.75em" h="1.75em" i-ri:close-line />
|
||||
</button>
|
||||
</CommonTooltip>
|
||||
</header>
|
||||
<ol ps-2 sm:ps-1>
|
||||
<li v-for="(error, i) in failedMessages" :key="i" flex="~ col sm:row" gap-y-1 sm:gap-x-2>
|
||||
<strong>{{ i + 1 }}.</strong>
|
||||
<span>{{ error }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</CommonErrorMessage>
|
||||
</div>
|
||||
</form>
|
||||
</MainContent>
|
||||
</template>
|
Loading…
Add table
Add a link
Reference in a new issue