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 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>
|
||||||
|
|
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
|
// @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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
// These look more like account and privacy settings than appearance settings
|
fieldsAttributes,
|
||||||
// discoverable: false,
|
|
||||||
// bot: false,
|
// These look more like account and privacy settings than appearance settings
|
||||||
// locked: false,
|
// discoverable: false,
|
||||||
}),
|
// bot: 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
|
||||||
|
|
Loading…
Reference in a new issue