208 lines
		
	
	
	
		
			5.7 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			208 lines
		
	
	
	
		
			5.7 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<script lang="ts" setup>
 | 
						|
import type { mastodon } from 'masto'
 | 
						|
import { useForm } from 'slimeform'
 | 
						|
import { parse } from 'ultrahtml'
 | 
						|
 | 
						|
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?.avatar || '',
 | 
						|
  header: account?.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?.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
 | 
						|
  }
 | 
						|
 | 
						|
  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="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>
 |