feat(front): Userprofile listening activity
This commit is contained in:
parent
fca4f3b77f
commit
4022f6a620
|
@ -12,6 +12,8 @@ import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||||
import TagsList from '~/components/tags/List.vue'
|
import TagsList from '~/components/tags/List.vue'
|
||||||
|
import Alert from '~/components/ui/Alert.vue'
|
||||||
|
import Spacer from '~/components/ui/Spacer.vue'
|
||||||
|
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
|
|
||||||
|
@ -101,16 +103,23 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
||||||
class="ui tiny circular label"
|
class="ui tiny circular label"
|
||||||
>{{ count }}</span>
|
>{{ count }}</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
<Alert
|
||||||
|
v-if="count === 0"
|
||||||
|
>
|
||||||
|
<i class="bi bi-music-note-list" />
|
||||||
|
{{ t('components.audio.track.Widget.empty.noResults') }}
|
||||||
|
<Loader v-if="isLoading" />
|
||||||
|
</Alert>
|
||||||
<div
|
<div
|
||||||
v-if="count > 0"
|
v-if="count > 0"
|
||||||
class="ui divided unstackable items"
|
|
||||||
>
|
>
|
||||||
<div
|
<div class="funkwhale activity"
|
||||||
v-for="object in objects"
|
v-for="object in objects"
|
||||||
:key="object.id"
|
:key="object.id"
|
||||||
:class="['item', itemClasses]"
|
:class="['item', itemClasses]"
|
||||||
|
@click="navigate('track')"
|
||||||
>
|
>
|
||||||
<div class="ui tiny image">
|
<div class="activity-image">
|
||||||
<img
|
<img
|
||||||
v-if="object.track.album && object.track.album.cover"
|
v-if="object.track.album && object.track.album.cover"
|
||||||
v-lazy="store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)"
|
v-lazy="store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)"
|
||||||
|
@ -129,26 +138,26 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
||||||
<img
|
<img
|
||||||
v-else
|
v-else
|
||||||
alt=""
|
alt=""
|
||||||
src="../../../assets/audio/default-cover.png"
|
src="../../../../public/embed-default-cover.jpeg"
|
||||||
>
|
>
|
||||||
<play-button
|
<PlayButton
|
||||||
class="play-overlay"
|
class="play-overlay"
|
||||||
:icon-only="true"
|
:icon-only="true"
|
||||||
:button-classes="['ui', 'circular', 'tiny', 'vibrant', 'icon', 'button']"
|
|
||||||
:track="object.track"
|
:track="object.track"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="middle aligned content">
|
<div class="activity-content">
|
||||||
<div class="ui unstackable grid">
|
<div class="track-title">
|
||||||
<div class="thirteen wide stretched column">
|
<router-link
|
||||||
<div class="ellipsis">
|
class="funkwhale link artist"
|
||||||
<router-link :to="{name: 'library.tracks.detail', params: {id: object.track.id}}">
|
:to="{name: 'library.tracks.detail', params: {id: object.track.id}}"
|
||||||
{{ object.track.title }}
|
>
|
||||||
</router-link>
|
{{ object.track.title }}
|
||||||
</div>
|
</router-link>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="object.track.artist_credit"
|
v-if="object.track.artist_credit"
|
||||||
class="meta ellipsis"
|
class="funkwhale link artist"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-for="ac in object.track.artist_credit"
|
v-for="ac in object.track.artist_credit"
|
||||||
|
@ -176,7 +185,7 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
||||||
class="extra"
|
class="extra"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
class="left floated"
|
class="funkwhale link user"
|
||||||
:to="{name: 'profile.overview', params: {username: object.actor.name}}"
|
:to="{name: 'profile.overview', params: {username: object.actor.name}}"
|
||||||
>
|
>
|
||||||
<span class="at symbol" />{{ object.actor.name }}
|
<span class="at symbol" />{{ object.actor.name }}
|
||||||
|
@ -184,47 +193,130 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
||||||
<span class="right floated"><human-date :date="object.creation_date" /></span>
|
<span class="right floated"><human-date :date="object.creation_date" /></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="one wide stretched column">
|
<play-button
|
||||||
<play-button
|
class="basic icon"
|
||||||
class="basic icon"
|
:account="object.actor"
|
||||||
:account="object.actor"
|
:dropdown-only="true"
|
||||||
:dropdown-only="true"
|
:dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']"
|
||||||
:dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']"
|
:track="object.track"
|
||||||
:track="object.track"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<Loader v-if="isLoading" />
|
||||||
v-if="isLoading"
|
|
||||||
class="ui inverted active dimmer"
|
|
||||||
>
|
|
||||||
<div class="ui loader" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="ui placeholder segment"
|
|
||||||
>
|
|
||||||
<div class="ui icon header">
|
|
||||||
<i class="music icon" />
|
|
||||||
{{ t('components.audio.track.Widget.empty.noResults') }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="isLoading"
|
|
||||||
class="ui inverted active dimmer"
|
|
||||||
>
|
|
||||||
<div class="ui loader" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template v-if="nextPage">
|
<template v-if="nextPage">
|
||||||
<div class="ui hidden divider" />
|
<Spacer :size="16"/>
|
||||||
<Button
|
<Button
|
||||||
|
secondary
|
||||||
@click="fetchData(nextPage as string)"
|
@click="fetchData(nextPage as string)"
|
||||||
>
|
>
|
||||||
{{ t('components.audio.track.Widget.button.more') }}
|
{{ t('components.audio.track.Widget.button.more') }}
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.funkwhale {
|
||||||
|
&.activity {
|
||||||
|
padding: 12px 0;
|
||||||
|
border-top: 1px solid;
|
||||||
|
@include light-theme {
|
||||||
|
border-color: var(--fw-gray-300);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
border-color: var(--fw-gray-800);
|
||||||
|
}
|
||||||
|
cursor: pointer;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
gap: 12px;
|
||||||
|
grid-column: span 4;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .activity-image {
|
||||||
|
|
||||||
|
width: 40px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--fw-border-radius);
|
||||||
|
|
||||||
|
> img {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .play-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin: 0;
|
||||||
|
border: 0 !important;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.play-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .activity-content {
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .track-title {
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.5em;
|
||||||
|
@include dark-theme {
|
||||||
|
color: var(--fw-gray-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user, time {
|
||||||
|
line-height: 1.5em;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--fw-gray-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
|
||||||
|
.play-button {
|
||||||
|
background: rgba(255, 255, 255, .5);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--fw-text-color: var(--fw-gray-800) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
|
||||||
|
.play-button {
|
||||||
|
background: rgba(0, 0, 0, .2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, .8);
|
||||||
|
--fw-text-color: var(--fw-gray-200) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -20,6 +20,11 @@ import Spacer from '~/components/ui/Spacer.vue'
|
||||||
import Link from '~/components/ui/Link.vue'
|
import Link from '~/components/ui/Link.vue'
|
||||||
import Tabs from '~/components/ui/Tabs.vue'
|
import Tabs from '~/components/ui/Tabs.vue'
|
||||||
import Tab from '~/components/ui/Tab.vue'
|
import Tab from '~/components/ui/Tab.vue'
|
||||||
|
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
||||||
|
import ChannelsWidget from '~/components/audio/ChannelsWidget.vue'
|
||||||
|
import PlaylistWidget from '~/components/playlists/Widget.vue'
|
||||||
|
import AlbumWidget from '~/components/album/Widget.vue'
|
||||||
|
import TrackWidget from '~/components/audio/track/Widget.vue'
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
(e: 'updated', value: Actor): void
|
(e: 'updated', value: Actor): void
|
||||||
|
@ -101,114 +106,6 @@ watch(props, fetchData, { immediate: true })
|
||||||
>
|
>
|
||||||
{{ store.state.auth.profile?.full_username?.[0] || "" }}
|
{{ store.state.auth.profile?.full_username?.[0] || "" }}
|
||||||
</span>
|
</span>
|
||||||
<!-- <div class="ui five wide column">
|
|
||||||
<button
|
|
||||||
ref="dropdown"
|
|
||||||
v-dropdown="{direction: 'downward'}"
|
|
||||||
class="ui pointing dropdown icon small basic right floated button"
|
|
||||||
style="position: absolute; right: 1em; top: 1em;"
|
|
||||||
>
|
|
||||||
<i class="ellipsis vertical icon" />
|
|
||||||
<div class="menu">
|
|
||||||
<a
|
|
||||||
v-if="object.domain != store.getters['instance/domain']"
|
|
||||||
:href="object.fid"
|
|
||||||
target="_blank"
|
|
||||||
class="basic item"
|
|
||||||
>
|
|
||||||
<i class="external icon" />
|
|
||||||
{{ t('views.auth.ProfileBase.link.domainView', {domain: object.domain}) }}
|
|
||||||
</a>
|
|
||||||
<div
|
|
||||||
v-for="obj in getReportableObjects({account: object})"
|
|
||||||
:key="obj.target.type + obj.target.id"
|
|
||||||
role="button"
|
|
||||||
class="basic item"
|
|
||||||
@click.stop.prevent="report(obj)"
|
|
||||||
>
|
|
||||||
<i class="share icon" /> {{ obj.label }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider" />
|
|
||||||
<router-link
|
|
||||||
v-if="store.state.auth.availablePermissions['moderation']"
|
|
||||||
class="basic item"
|
|
||||||
:to="{name: 'manage.moderation.accounts.detail', params: {id: object.full_username}}"
|
|
||||||
>
|
|
||||||
<i class="wrench icon" />
|
|
||||||
{{ t('views.auth.ProfileBase.link.moderation') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<user-follow-button
|
|
||||||
v-if="store.state.auth.authenticated && object && object.full_username !== store.state.auth.fullUsername"
|
|
||||||
:actor="object"
|
|
||||||
/>
|
|
||||||
<h1 class="ui center aligned icon header">
|
|
||||||
<i
|
|
||||||
v-if="!object.icon"
|
|
||||||
class="circular inverted user success icon"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
v-else
|
|
||||||
v-lazy="store.getters['instance/absoluteUrl'](object.icon.urls.medium_square_crop)"
|
|
||||||
alt=""
|
|
||||||
class="ui big circular image"
|
|
||||||
>
|
|
||||||
<div class="ellispsis content">
|
|
||||||
<div class="ui very small hidden divider" />
|
|
||||||
<span>{{ displayName }}</span>
|
|
||||||
<div class="ui very small hidden divider" />
|
|
||||||
<div
|
|
||||||
class="sub header ellipsis"
|
|
||||||
:title="object.full_username"
|
|
||||||
>
|
|
||||||
{{ object.full_username }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template v-if="object.full_username === store.state.auth.fullUsername">
|
|
||||||
<div class="ui very small hidden divider" />
|
|
||||||
<div class="ui basic success label">
|
|
||||||
{{ t('views.auth.ProfileBase.label.self') }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</h1>
|
|
||||||
<div class="ui small hidden divider" />
|
|
||||||
<div v-if="store.getters['ui/layoutVersion'] === 'large'">
|
|
||||||
<rendered-description
|
|
||||||
:content="object.summary"
|
|
||||||
:field-name="'summary'"
|
|
||||||
:update-url="`users/${store.state.auth.username}/`"
|
|
||||||
:can-update="store.state.auth.authenticated && object.full_username === store.state.auth.fullUsername"
|
|
||||||
@updated="emit('updated', $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui eleven wide column">
|
|
||||||
<div class="ui head vertical stripe segment">
|
|
||||||
<div class="ui container">
|
|
||||||
<div class="ui secondary pointing center aligned menu">
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'profile.overview', params: routerParams}"
|
|
||||||
>
|
|
||||||
{{ t('views.auth.ProfileBase.link.overview') }}
|
|
||||||
</router-link>
|
|
||||||
<router-link
|
|
||||||
class="item"
|
|
||||||
:to="{name: 'profile.activity', params: routerParams}"
|
|
||||||
>
|
|
||||||
{{ t('views.auth.ProfileBase.link.activity') }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="ui hidden divider" />
|
|
||||||
<router-view
|
|
||||||
:object="object"
|
|
||||||
@updated="fetchData"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
<!-- TODO: Translate Edit Link -->
|
<!-- TODO: Translate Edit Link -->
|
||||||
<Section no-items
|
<Section no-items
|
||||||
:h1="store.state.auth.username"
|
:h1="store.state.auth.username"
|
||||||
|
@ -235,28 +132,72 @@ watch(props, fetchData, { immediate: true })
|
||||||
</Layout>
|
</Layout>
|
||||||
<!-- TODO: Make routerlinks work for tabs -->
|
<!-- TODO: Make routerlinks work for tabs -->
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Overview" :to="{name: 'profile.overview', params: routerParams}">
|
<Tab :title="t('views.auth.ProfileBase.link.overview')" :to="{name: 'profile.overview', params: routerParams}">
|
||||||
{{ t('views.auth.ProfileBase.link.overview') }}
|
<h2>{{ t('views.auth.ProfileBase.link.overview') }}</h2>
|
||||||
<router-view
|
|
||||||
:object="object"
|
|
||||||
@updated="fetchData"
|
|
||||||
/>
|
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="Collections">
|
<Tab :title="t('views.auth.ProfileOverview.header.libraries')">
|
||||||
{{ t('views.auth.ProfileBase.link.collections') }}
|
<h2 class="ui with-actions header">
|
||||||
|
{{ t('views.auth.ProfileOverview.header.libraries') }}
|
||||||
|
<div
|
||||||
|
v-if="store.state.auth.authenticated && object.full_username === store.state.auth.fullUsername"
|
||||||
|
class="actions"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
<library-widget :url="`federation/actors/${object.full_username}/libraries/`">
|
||||||
|
<template #title>
|
||||||
|
{{ t('views.auth.ProfileOverview.header.sharedLibraries') }}
|
||||||
|
</template>
|
||||||
|
</library-widget>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="Channels">
|
<Tab :title="t('views.auth.ProfileOverview.header.channels')">
|
||||||
{{ t('views.auth.ProfileBase.link.channels') }}
|
<h2 class="ui with-actions header">
|
||||||
|
{{ t('views.auth.ProfileOverview.header.channels') }}
|
||||||
|
<div
|
||||||
|
v-if="store.state.auth.authenticated && object.full_username === store.state.auth.fullUsername"
|
||||||
|
class="actions"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
@click.stop.prevent="showUploadModal = true"
|
||||||
|
>
|
||||||
|
<i class="bi bi-plus" />
|
||||||
|
{{ t('views.auth.ProfileOverview.link.addNew') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
<channels-widget :filters="{scope: `actor:${object.full_username}`}" />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="Activity" :to="{name: 'profile.activity', params: routerParams}">
|
<Tab :title="t('views.auth.ProfileBase.link.activity')" :to="{name: 'profile.activity', params: routerParams}">
|
||||||
{{ t('views.auth.ProfileBase.link.activity') }}
|
|
||||||
<router-view
|
<track-widget
|
||||||
:object="object"
|
:url="'history/listenings/'"
|
||||||
@updated="fetchData"
|
:filters="{ scope, ordering: '-creation_date', ...qualityFilters}"
|
||||||
/>
|
:websocket-handlers="['Listen']"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
{{ t('components.library.Home.header.recentlyListened') }}
|
||||||
|
</template>
|
||||||
|
</track-widget>
|
||||||
|
|
||||||
|
<track-widget
|
||||||
|
:url="'favorites/tracks/'"
|
||||||
|
:filters="{scope: scope, ordering: '-creation_date'}"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
{{ t('components.library.Home.header.recentlyFavorited') }}
|
||||||
|
</template>
|
||||||
|
</track-widget>
|
||||||
|
|
||||||
|
<album-widget :filters="{scope: scope, playable: true, ordering: '-creation_date', ...qualityFilters}">
|
||||||
|
<template #title>
|
||||||
|
{{ t('components.library.Home.header.recentlyAdded') }}
|
||||||
|
</template>
|
||||||
|
</album-widget>
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
Loading…
Reference in New Issue