270 lines
7.1 KiB
Vue
270 lines
7.1 KiB
Vue
<script setup lang="ts">
|
|
import type { components } from '~/generated/types'
|
|
|
|
import { onBeforeRouteUpdate } from 'vue-router'
|
|
import { computed, ref, watch } from 'vue'
|
|
import { useClipboard } from '@vueuse/core'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useStore } from '~/store'
|
|
import { hashCode, intToRGB } from '~/utils/color'
|
|
|
|
import UserFollowButton from '~/components/federation/UserFollowButton.vue'
|
|
import { useModal } from '~/ui/composables/useModal.ts'
|
|
|
|
import axios from 'axios'
|
|
|
|
import useErrorHandler from '~/composables/useErrorHandler'
|
|
import RenderedDescription from '~/components/common/RenderedDescription.vue'
|
|
|
|
import Layout from '~/components/ui/Layout.vue'
|
|
import Spacer from '~/components/ui/Spacer.vue'
|
|
import Header from '~/components/ui/Header.vue'
|
|
import Button from '~/components/ui/Button.vue'
|
|
import Link from '~/components/ui/Link.vue'
|
|
import Nav from '~/components/ui/Nav.vue'
|
|
import Alert from '~/components/ui/Alert.vue'
|
|
import Modal from '~/components/ui/Modal.vue'
|
|
|
|
interface Events {
|
|
(e: 'updated', value: components['schemas']['FullActor']): void
|
|
}
|
|
|
|
interface Props {
|
|
username: string
|
|
domain?: string | null
|
|
}
|
|
|
|
const emit = defineEmits<Events>()
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
domain: null
|
|
})
|
|
|
|
const store = useStore()
|
|
|
|
const object = ref<components['schemas']['FullActor'] | null>(null)
|
|
|
|
const actorColor = computed(() => intToRGB(hashCode(object.value?.full_username)))
|
|
const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${actorColor.value}` }))
|
|
|
|
// TODO: Check if still needed
|
|
//const displayName = computed(() => object.value?.name ?? object.value?.preferred_username)
|
|
|
|
const fullUsername = computed(() => props.domain
|
|
? `${props.username}@${props.domain}`
|
|
: `${props.username}@${store.getters['instance/domain']}`
|
|
)
|
|
|
|
const routerParams = computed(() => props.domain
|
|
? { username: props.username, domain: props.domain }
|
|
: { username: props.username }
|
|
)
|
|
|
|
const { t } = useI18n()
|
|
const labels = computed(() => ({
|
|
usernameProfile: t('views.auth.ProfileBase.title', { username: props.username })
|
|
}))
|
|
|
|
onBeforeRouteUpdate((to) => {
|
|
to.meta.preserveScrollPosition = true
|
|
})
|
|
|
|
const isLoading = ref(false)
|
|
const fetchData = async () => {
|
|
object.value = null
|
|
isLoading.value = true
|
|
|
|
try {
|
|
const response = await axios.get(`federation/actors/${fullUsername.value}/`)
|
|
object.value = response.data
|
|
} catch (error) {
|
|
useErrorHandler(error as Error)
|
|
}
|
|
|
|
isLoading.value = false
|
|
}
|
|
|
|
watch(props, fetchData, { immediate: true })
|
|
|
|
const { copy, copied, isSupported } = useClipboard()
|
|
|
|
const tabs = ref([{
|
|
title: t('views.auth.ProfileBase.link.overview') ,
|
|
to: { name: 'profile.overview', params: routerParams }
|
|
}, {
|
|
title: t('views.auth.ProfileBase.link.activity') ,
|
|
to: { name: 'profile.activity', params: routerParams }
|
|
}, ...(
|
|
store.state.auth.authenticated && fullUsername.value === store.state.auth.fullUsername
|
|
? [{
|
|
title: t('views.auth.ProfileBase.link.manageUploads') ,
|
|
to: { name: 'profile.manageUploads', params: routerParams }
|
|
}]
|
|
: []
|
|
)])
|
|
|
|
const isOpen = useModal('artist-description').isOpen
|
|
</script>
|
|
|
|
<template>
|
|
<Layout
|
|
v-title="labels.usernameProfile"
|
|
stack
|
|
main
|
|
no-gap
|
|
>
|
|
<!-- TODO: Translate Edit Link -->
|
|
<!-- TODO: `yarn lint:tsc` doesn't understand the `Prop` type for `Header` while the language server does. It may be a question of typescript version... Investigate and fix! https://dev.funkwhale.audio/funkwhale/funkwhale/-/issues/2437 -->
|
|
<!-- @vue-ignore -->
|
|
<Header
|
|
:h1="props.username"
|
|
:action="{
|
|
text: t('views.auth.ProfileBase.link.edit'),
|
|
// @ts-ignore
|
|
to:'/settings',
|
|
// @ts-ignore
|
|
primary: true,
|
|
// @ts-ignore
|
|
solid: true,
|
|
// @ts-ignore
|
|
icon: 'bi-pencil-fill',
|
|
// @ts-ignore
|
|
lowHeight: true
|
|
}"
|
|
no-gap
|
|
page-heading
|
|
>
|
|
<template #image>
|
|
<img
|
|
v-if="object?.user?.avatar"
|
|
v-lazy="store.getters['instance/absoluteUrl'](object.user.avatar.urls.large_square_crop)"
|
|
alt=""
|
|
class="avatar"
|
|
>
|
|
<span
|
|
v-else
|
|
:style="defaultAvatarStyle"
|
|
class="ui avatar circular label"
|
|
>
|
|
{{ fullUsername[0] || "" }}
|
|
</span>
|
|
</template>
|
|
<Layout flex>
|
|
<span style="grid-column: 1 / -1">
|
|
{{ fullUsername }}
|
|
<Button
|
|
icon="bi-copy"
|
|
style="margin-left: 8px;"
|
|
:aria-label="t('views.auth.ProfileBase.copyUsername')"
|
|
:title="t('components.common.CopyInput.button.copy')"
|
|
ghost
|
|
secondary
|
|
low-height
|
|
@click="copy(fullUsername)"
|
|
/>
|
|
</span>
|
|
<Alert
|
|
v-if="isSupported && copied"
|
|
green
|
|
>
|
|
<Layout flex>
|
|
<i class="bi bi-check" />
|
|
{{ t('components.common.CopyInput.message.success') }}
|
|
</Layout>
|
|
</Alert>
|
|
<Alert
|
|
v-else-if="!isSupported && copied"
|
|
red
|
|
>
|
|
<Layout flex>
|
|
<i class="bi bi-x" />
|
|
{{ t('components.common.CopyInput.message.fail') }}
|
|
</Layout>
|
|
</Alert>
|
|
</Layout>
|
|
<Layout
|
|
flex
|
|
no-gap
|
|
>
|
|
<RenderedDescription
|
|
v-if="object?.summary"
|
|
class="description"
|
|
:content="{ html: object?.summary.html || '' }"
|
|
:truncate-length="100"
|
|
:more-link="false"
|
|
/>
|
|
<Spacer grow />
|
|
<Link
|
|
v-if="object?.summary"
|
|
:to="useModal('artist-description').to"
|
|
style="color: var(--fw-primary); text-decoration: underline;"
|
|
thin-font
|
|
force-underline
|
|
>
|
|
{{ t('components.common.RenderedDescription.button.more') }}
|
|
</Link>
|
|
</Layout>
|
|
<Modal
|
|
v-if="object?.summary"
|
|
v-model="isOpen"
|
|
:title="object?.name"
|
|
>
|
|
<img
|
|
v-if="object?.user.avatar"
|
|
v-lazy="object?.user.avatar.urls.original"
|
|
:alt="object?.name"
|
|
style="object-fit: cover; width: 100%; height: 100%;"
|
|
>
|
|
<sanitized-html
|
|
v-if="object?.summary"
|
|
:html="object?.summary.html"
|
|
/>
|
|
</Modal>
|
|
<UserFollowButton
|
|
v-if="store.state.auth.authenticated && fullUsername !== store.state.auth.fullUsername && object"
|
|
low-height
|
|
:actor="object"
|
|
/>
|
|
</Header>
|
|
<Nav v-model="tabs" />
|
|
|
|
<router-view
|
|
:object="object"
|
|
@updated="fetchData"
|
|
/>
|
|
</Layout>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
img.avatar {
|
|
width: 200px;
|
|
height: 200px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
span.avatar {
|
|
display: block;
|
|
font-size: 150px;
|
|
font-style: normal;
|
|
font-weight: 800;
|
|
text-align: center;
|
|
align-content: center;
|
|
background-color: var(--fw-gray-500);
|
|
border-radius: 50%;
|
|
width: 200px;
|
|
height: 200px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 48px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
a.edit span {
|
|
color: var(--fw-primary);
|
|
|
|
&:hover {
|
|
color: var(--color);
|
|
}
|
|
}
|
|
</style>
|