feat(settings): metadata (#699)

Co-authored-by: LittleSound <464388324@qq.com>
This commit is contained in:
三咲智子 Kevin Deng 2023-01-02 23:00:11 +08:00 committed by GitHub
parent f942ddc5a3
commit c216c81bb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 146 additions and 60 deletions

View file

@ -17,10 +17,6 @@ const createdAt = $(useFormattedDateTime(() => account.createdAt, {
const namedFields = ref<Field[]>([]) const namedFields = ref<Field[]>([])
const iconFields = ref<Field[]>([]) const iconFields = ref<Field[]>([])
function getFieldNameIcon(fieldName: string) {
const name = fieldName.trim().toLowerCase()
return ACCOUNT_FIELD_ICONS[name] || undefined
}
function getFieldIconTitle(fieldName: string) { function getFieldIconTitle(fieldName: string) {
return fieldName === 'Joined' ? t('account.joined') : fieldName return fieldName === 'Joined' ? t('account.joined') : fieldName
} }
@ -48,7 +44,7 @@ watchEffect(() => {
const icons: Field[] = [] const icons: Field[] = []
account.fields?.forEach((field) => { account.fields?.forEach((field) => {
const icon = getFieldNameIcon(field.name) const icon = getAccountFieldIcon(field.name)
if (icon) if (icon)
icons.push(field) icons.push(field)
else else
@ -122,7 +118,7 @@ const isSelf = $computed(() => currentUser.value?.account.id === account.id)
</div> </div>
<div v-if="iconFields.length" flex="~ wrap gap-4"> <div v-if="iconFields.length" flex="~ wrap gap-4">
<div v-for="field in iconFields" :key="field.name" flex="~ gap-1" items-center> <div v-for="field in iconFields" :key="field.name" flex="~ gap-1" items-center>
<div text-secondary :class="getFieldNameIcon(field.name)" :title="getFieldIconTitle(field.name)" /> <div text-secondary :class="getAccountFieldIcon(field.name)" :title="getFieldIconTitle(field.name)" />
<ContentRich text-sm filter-saturate-0 :content="field.value" :emojis="account.emojis" /> <ContentRich text-sm filter-saturate-0 :content="field.value" :emojis="account.emojis" />
</div> </div>
</div> </div>

View file

@ -0,0 +1,58 @@
<script setup lang="ts">
import type { UpdateCredentialsParams } from 'masto'
const { form } = defineModel<{
form: {
fieldsAttributes: NonNullable<UpdateCredentialsParams['fieldsAttributes']>
}
}>()
const fieldIcons = computed(() =>
Array.from({ length: 4 }, (_, i) =>
getAccountFieldIcon(form.value.fieldsAttributes[i].name),
),
)
</script>
<template>
<div flex="~ col gap4">
<div v-for="i in 4" :key="i" flex="~ gap3" items-center>
<CommonDropdown placement="left">
<CommonTooltip content="Pick a icon">
<button btn-action-icon>
<div :class="fieldIcons[i - 1] || 'i-ri:question-mark'" />
</button>
</CommonTooltip>
<template #popper>
<div flex="~ wrap gap-1" max-w-50 m2>
<CommonTooltip
v-for="(icon, text) in accountFieldIcons"
:key="icon"
:content="text"
>
<template v-if="text !== 'Joined'">
<div btn-action-icon @click="form.fieldsAttributes[i - 1].name = text">
<div text-xl :class="icon" />
</div>
</template>
</CommonTooltip>
</div>
</template>
</CommonDropdown>
<input
v-model="form.fieldsAttributes[i - 1].name"
type="text"
p2 border-rounded w-full bg-transparent
outline-none border="~ base"
placeholder="Label"
>
<input
v-model="form.fieldsAttributes[i - 1].value"
type="text"
p2 border-rounded w-full bg-transparent
outline-none border="~ base"
placeholder="Content"
>
</div>
</div>
</template>

View file

@ -1,41 +1,52 @@
// @unocss-include // @unocss-include
export const ACCOUNT_FIELD_ICONS: Record<string, string> = { export const accountFieldIcons: Record<string, string> = Object.fromEntries(Object.entries({
alipay: 'i-ri:alipay-fill', Alipay: 'i-ri:alipay-fill',
bilibili: 'i-ri:bilibili-fill', Bilibili: 'i-ri:bilibili-fill',
birth: 'i-ri:calendar-line', Birth: 'i-ri:calendar-line',
blog: 'i-ri:newspaper-line', Blog: 'i-ri:newspaper-line',
city: 'i-ri:map-pin-2-line', City: 'i-ri:map-pin-2-line',
dingding: 'i-ri:dingding-fill', Dingding: 'i-ri:dingding-fill',
discord: 'i-ri:discord-fill', Discord: 'i-ri:discord-fill',
douban: 'i-ri:douban-fill', Douban: 'i-ri:douban-fill',
facebook: 'i-ri:facebook-fill', Facebook: 'i-ri:facebook-fill',
github: 'i-ri:github-fill', GitHub: 'i-ri:github-fill',
gitlab: 'i-ri:gitlab-fill', GitLab: 'i-ri:gitlab-fill',
home: 'i-ri:home-2-line', Home: 'i-ri:home-2-line',
instagram: 'i-ri:instagram-line', Instagram: 'i-ri:instagram-line',
joined: 'i-ri:user-add-line', Joined: 'i-ri:user-add-line',
linkedin: 'i-ri:linkedin-box-fill', LinkedIn: 'i-ri:linkedin-box-fill',
location: 'i-ri:map-pin-2-line', Location: 'i-ri:map-pin-2-line',
mastodon: 'i-ri:mastodon-line', Mastodon: 'i-ri:mastodon-line',
medium: 'i-ri:medium-fill', Medium: 'i-ri:medium-fill',
patreon: 'i-ri:patreon-fill', Patreon: 'i-ri:patreon-fill',
paypal: 'i-ri:paypal-fill', PayPal: 'i-ri:paypal-fill',
playstation: 'i-ri:playstation-fill', PlayStation: 'i-ri:playstation-fill',
portfolio: 'i-ri:link', Portfolio: 'i-ri:link',
qq: 'i-ri:qq-fill', QQ: 'i-ri:qq-fill',
site: 'i-ri:link', Site: 'i-ri:link',
sponsors: 'i-ri:heart-3-line', Sponsors: 'i-ri:heart-3-line',
spotify: 'i-ri:spotify-fill', Spotify: 'i-ri:spotify-fill',
steam: 'i-ri:steam-fill', Steam: 'i-ri:steam-fill',
switch: 'i-ri:switch-fill', Switch: 'i-ri:switch-fill',
telegram: 'i-ri:telegram-fill', Telegram: 'i-ri:telegram-fill',
tumblr: 'i-ri:tumblr-fill', Tumblr: 'i-ri:tumblr-fill',
twitch: 'i-ri:twitch-line', Twitch: 'i-ri:twitch-line',
twitter: 'i-ri:twitter-line', Twitter: 'i-ri:twitter-line',
website: 'i-ri:link', Website: 'i-ri:link',
wechat: 'i-ri:wechat-fill', WeChat: 'i-ri:wechat-fill',
weibo: 'i-ri:weibo-fill', Weibo: 'i-ri:weibo-fill',
xbox: 'i-ri:xbox-fill', Xbox: 'i-ri:xbox-fill',
youtube: 'i-ri:youtube-line', YouTube: 'i-ri:youtube-line',
zhihu: 'i-ri:zhihu-fill', Zhihu: 'i-ri:zhihu-fill',
}).sort(([a], [b]) => a.localeCompare(b)))
const accountFieldIconsLowercase = Object.fromEntries(
Object.entries(accountFieldIcons).map(([k, v]) =>
[k.toLowerCase(), v],
),
)
export const getAccountFieldIcon = (value: string) => {
const name = value.trim().toLowerCase()
return accountFieldIconsLowercase[name] || undefined
} }

View file

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { UpdateCredentialsParams } from 'masto'
import { useForm } from 'slimeform' import { useForm } from 'slimeform'
definePageMeta({ definePageMeta({
@ -7,26 +8,34 @@ definePageMeta({
keepalive: false, keepalive: false,
}) })
const acccount = $computed(() => currentUser.value?.account) const account = $computed(() => currentUser.value?.account)
const onlineSrc = $computed(() => ({ const onlineSrc = $computed(() => ({
avatar: acccount?.avatar || '', avatar: account?.avatar || '',
header: acccount?.header || '', header: account?.header || '',
})) }))
const { form, reset, submitter, dirtyFields, isError } = useForm({ const { form, reset, submitter, dirtyFields, isError } = useForm({
form: () => ({ form: () => {
displayName: acccount?.displayName ?? '', // For complex types of objects, a deep copy is required to ensure correct comparison of initial and modified values
note: acccount?.source.note.replaceAll('\r', '') ?? '', const fieldsAttributes = Array.from({ length: 4 }, (_, i) => {
return { ...account?.fields?.[i] || { name: '', value: '' } }
})
return {
displayName: account?.displayName ?? '',
note: account?.source.note.replaceAll('\r', '') ?? '',
avatar: null as null | File, avatar: null as null | File,
header: null as null | File, header: null as null | File,
fieldsAttributes,
// These look more like account and privacy settings than appearance settings // These look more like account and privacy settings than appearance settings
// discoverable: false, // discoverable: false,
// bot: false, // bot: false,
// locked: false, // locked: false,
}), }
},
}) })
watch(isMastoInitialised, async (val) => { watch(isMastoInitialised, async (val) => {
@ -41,7 +50,7 @@ watch(isMastoInitialised, async (val) => {
const isCanSubmit = computed(() => !isError.value && !isEmptyObject(dirtyFields.value)) const isCanSubmit = computed(() => !isError.value && !isEmptyObject(dirtyFields.value))
const { submit, submitting } = submitter(async ({ dirtyFields }) => { const { submit, submitting } = submitter(async ({ dirtyFields }) => {
const res = await useMasto().accounts.updateCredentials(dirtyFields.value) const res = await useMasto().accounts.updateCredentials(dirtyFields.value as UpdateCredentialsParams)
.then(account => ({ account })) .then(account => ({ account }))
.catch((error: Error) => ({ error })) .catch((error: Error) => ({ error }))
@ -51,7 +60,7 @@ const { submit, submitting } = submitter(async ({ dirtyFields }) => {
return return
} }
setAccountInfo(acccount!.id, res.account) setAccountInfo(account!.id, res.account)
reset() reset()
}) })
</script> </script>
@ -108,6 +117,18 @@ const { submit, submitting } = submitter(async ({ dirtyFields }) => {
<textarea v-model="form.note" maxlength="500" min-h-10ex input-base /> <textarea v-model="form.note" maxlength="500" min-h-10ex input-base />
</label> </label>
<!-- metadata -->
<div space-y-2>
<div font-medium>
Profile metadata
</div>
<div text-sm text-secondary>
You can have up to 4 items displayed as a table on your profile
</div>
<SettingsProfileMetadata v-if="isHydrated" v-model:form="form" />
</div>
<!-- submit --> <!-- submit -->
<div text-right> <div text-right>
<button <button