forked from Mirrors/elk
feat(settings): metadata (#699)
Co-authored-by: LittleSound <464388324@qq.com>
This commit is contained in:
parent
f942ddc5a3
commit
c216c81bb7
4 changed files with 146 additions and 60 deletions
|
@ -17,10 +17,6 @@ const createdAt = $(useFormattedDateTime(() => account.createdAt, {
|
|||
const namedFields = 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) {
|
||||
return fieldName === 'Joined' ? t('account.joined') : fieldName
|
||||
}
|
||||
|
@ -48,7 +44,7 @@ watchEffect(() => {
|
|||
const icons: Field[] = []
|
||||
|
||||
account.fields?.forEach((field) => {
|
||||
const icon = getFieldNameIcon(field.name)
|
||||
const icon = getAccountFieldIcon(field.name)
|
||||
if (icon)
|
||||
icons.push(field)
|
||||
else
|
||||
|
@ -122,7 +118,7 @@ const isSelf = $computed(() => currentUser.value?.account.id === account.id)
|
|||
</div>
|
||||
<div v-if="iconFields.length" flex="~ wrap gap-4">
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
58
components/settings/SettingsProfileMetadata.vue
Normal file
58
components/settings/SettingsProfileMetadata.vue
Normal 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>
|
|
@ -1,41 +1,52 @@
|
|||
// @unocss-include
|
||||
export const ACCOUNT_FIELD_ICONS: Record<string, string> = {
|
||||
alipay: 'i-ri:alipay-fill',
|
||||
bilibili: 'i-ri:bilibili-fill',
|
||||
birth: 'i-ri:calendar-line',
|
||||
blog: 'i-ri:newspaper-line',
|
||||
city: 'i-ri:map-pin-2-line',
|
||||
dingding: 'i-ri:dingding-fill',
|
||||
discord: 'i-ri:discord-fill',
|
||||
douban: 'i-ri:douban-fill',
|
||||
facebook: 'i-ri:facebook-fill',
|
||||
github: 'i-ri:github-fill',
|
||||
gitlab: 'i-ri:gitlab-fill',
|
||||
home: 'i-ri:home-2-line',
|
||||
instagram: 'i-ri:instagram-line',
|
||||
joined: 'i-ri:user-add-line',
|
||||
linkedin: 'i-ri:linkedin-box-fill',
|
||||
location: 'i-ri:map-pin-2-line',
|
||||
mastodon: 'i-ri:mastodon-line',
|
||||
medium: 'i-ri:medium-fill',
|
||||
patreon: 'i-ri:patreon-fill',
|
||||
paypal: 'i-ri:paypal-fill',
|
||||
playstation: 'i-ri:playstation-fill',
|
||||
portfolio: 'i-ri:link',
|
||||
qq: 'i-ri:qq-fill',
|
||||
site: 'i-ri:link',
|
||||
sponsors: 'i-ri:heart-3-line',
|
||||
spotify: 'i-ri:spotify-fill',
|
||||
steam: 'i-ri:steam-fill',
|
||||
switch: 'i-ri:switch-fill',
|
||||
telegram: 'i-ri:telegram-fill',
|
||||
tumblr: 'i-ri:tumblr-fill',
|
||||
twitch: 'i-ri:twitch-line',
|
||||
twitter: 'i-ri:twitter-line',
|
||||
website: 'i-ri:link',
|
||||
wechat: 'i-ri:wechat-fill',
|
||||
weibo: 'i-ri:weibo-fill',
|
||||
xbox: 'i-ri:xbox-fill',
|
||||
youtube: 'i-ri:youtube-line',
|
||||
zhihu: 'i-ri:zhihu-fill',
|
||||
export const accountFieldIcons: Record<string, string> = Object.fromEntries(Object.entries({
|
||||
Alipay: 'i-ri:alipay-fill',
|
||||
Bilibili: 'i-ri:bilibili-fill',
|
||||
Birth: 'i-ri:calendar-line',
|
||||
Blog: 'i-ri:newspaper-line',
|
||||
City: 'i-ri:map-pin-2-line',
|
||||
Dingding: 'i-ri:dingding-fill',
|
||||
Discord: 'i-ri:discord-fill',
|
||||
Douban: 'i-ri:douban-fill',
|
||||
Facebook: 'i-ri:facebook-fill',
|
||||
GitHub: 'i-ri:github-fill',
|
||||
GitLab: 'i-ri:gitlab-fill',
|
||||
Home: 'i-ri:home-2-line',
|
||||
Instagram: 'i-ri:instagram-line',
|
||||
Joined: 'i-ri:user-add-line',
|
||||
LinkedIn: 'i-ri:linkedin-box-fill',
|
||||
Location: 'i-ri:map-pin-2-line',
|
||||
Mastodon: 'i-ri:mastodon-line',
|
||||
Medium: 'i-ri:medium-fill',
|
||||
Patreon: 'i-ri:patreon-fill',
|
||||
PayPal: 'i-ri:paypal-fill',
|
||||
PlayStation: 'i-ri:playstation-fill',
|
||||
Portfolio: 'i-ri:link',
|
||||
QQ: 'i-ri:qq-fill',
|
||||
Site: 'i-ri:link',
|
||||
Sponsors: 'i-ri:heart-3-line',
|
||||
Spotify: 'i-ri:spotify-fill',
|
||||
Steam: 'i-ri:steam-fill',
|
||||
Switch: 'i-ri:switch-fill',
|
||||
Telegram: 'i-ri:telegram-fill',
|
||||
Tumblr: 'i-ri:tumblr-fill',
|
||||
Twitch: 'i-ri:twitch-line',
|
||||
Twitter: 'i-ri:twitter-line',
|
||||
Website: 'i-ri:link',
|
||||
WeChat: 'i-ri:wechat-fill',
|
||||
Weibo: 'i-ri:weibo-fill',
|
||||
Xbox: 'i-ri:xbox-fill',
|
||||
YouTube: 'i-ri:youtube-line',
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import type { UpdateCredentialsParams } from 'masto'
|
||||
import { useForm } from 'slimeform'
|
||||
|
||||
definePageMeta({
|
||||
|
@ -7,26 +8,34 @@ definePageMeta({
|
|||
keepalive: false,
|
||||
})
|
||||
|
||||
const acccount = $computed(() => currentUser.value?.account)
|
||||
const account = $computed(() => currentUser.value?.account)
|
||||
|
||||
const onlineSrc = $computed(() => ({
|
||||
avatar: acccount?.avatar || '',
|
||||
header: acccount?.header || '',
|
||||
avatar: account?.avatar || '',
|
||||
header: account?.header || '',
|
||||
}))
|
||||
|
||||
const { form, reset, submitter, dirtyFields, isError } = useForm({
|
||||
form: () => ({
|
||||
displayName: acccount?.displayName ?? '',
|
||||
note: acccount?.source.note.replaceAll('\r', '') ?? '',
|
||||
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: 4 }, (_, i) => {
|
||||
return { ...account?.fields?.[i] || { name: '', value: '' } }
|
||||
})
|
||||
return {
|
||||
displayName: account?.displayName ?? '',
|
||||
note: account?.source.note.replaceAll('\r', '') ?? '',
|
||||
|
||||
avatar: null as null | File,
|
||||
header: null as null | File,
|
||||
avatar: null as null | File,
|
||||
header: null as null | File,
|
||||
|
||||
// These look more like account and privacy settings than appearance settings
|
||||
// discoverable: false,
|
||||
// bot: false,
|
||||
// locked: false,
|
||||
}),
|
||||
fieldsAttributes,
|
||||
|
||||
// These look more like account and privacy settings than appearance settings
|
||||
// discoverable: false,
|
||||
// bot: false,
|
||||
// locked: false,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
watch(isMastoInitialised, async (val) => {
|
||||
|
@ -41,7 +50,7 @@ watch(isMastoInitialised, async (val) => {
|
|||
const isCanSubmit = computed(() => !isError.value && !isEmptyObject(dirtyFields.value))
|
||||
|
||||
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 }))
|
||||
.catch((error: Error) => ({ error }))
|
||||
|
||||
|
@ -51,7 +60,7 @@ const { submit, submitting } = submitter(async ({ dirtyFields }) => {
|
|||
return
|
||||
}
|
||||
|
||||
setAccountInfo(acccount!.id, res.account)
|
||||
setAccountInfo(account!.id, res.account)
|
||||
reset()
|
||||
})
|
||||
</script>
|
||||
|
@ -108,6 +117,18 @@ const { submit, submitting } = submitter(async ({ dirtyFields }) => {
|
|||
<textarea v-model="form.note" maxlength="500" min-h-10ex input-base />
|
||||
</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 -->
|
||||
<div text-right>
|
||||
<button
|
||||
|
|
Loading…
Reference in a new issue