koel/resources/assets/js/components/podcast/EpisodeItem.vue

132 lines
4.7 KiB
Vue
Raw Normal View History

2024-05-19 05:49:42 +00:00
<template>
<a
data-testid="episode-item"
class="group relative flex flex-col md:flex-row gap-4 px-6 py-5 !text-k-text-primary hover:bg-white/10 duration-200"
:class="isCurrentEpisode && 'current'"
:href="`/#/episodes/${episode.id}`"
@contextmenu.prevent="requestContextMenu"
@dragstart="onDragStart"
>
<Icon v-if="isCurrentEpisode" :icon="faBookmark" size="xl" class="absolute -top-1 right-3 text-k-accent" />
2024-05-19 05:49:42 +00:00
<button
2024-05-31 05:06:19 +00:00
class="hidden md:block md:flex-[0_0_128px] relative overflow-hidden rounded-lg active:scale-95"
2024-05-19 05:49:42 +00:00
role="button"
@click.prevent="playOrPause"
>
<img
:src="episode.episode_image"
alt="Episode thumbnail"
class="w-[128px] aspect-square object-cover"
loading="lazy"
>
2024-05-19 05:49:42 +00:00
<span class="absolute top-0 left-0 w-full h-full group-hover:bg-black/40 z-10" />
<span
class="absolute flex opacity-0 items-center justify-center w-[48px] aspect-square rounded-full top-1/2
left-1/2 -translate-x-1/2 -translate-y-1/2 bg-k-highlight group-hover:opacity-100 duration-500 transition z-20"
>
<Icon v-if="isPlaying" :icon="faPause" class="text-white" size="2xl" />
<Icon v-else :icon="faPlay" class="ml-1 text-white" size="2xl" />
</span>
</button>
<div class="flex-1">
<time
:datetime="episode.created_at"
:title="episode.created_at"
class="block uppercase text-sm mb-1 text-k-text-secondary"
>
{{ publicationDateForHumans }}
</time>
<h3 class="text-xl" :title="episode.title">{{ episode.title }}</h3>
<div class="description text-k-text-secondary mt-3 line-clamp-3" v-html="description" />
</div>
<div class="md:flex-[0_0_122px] text-sm text-k-text-secondary flex md:flex-col items-center justify-center w-full">
2024-05-19 05:49:42 +00:00
<span class="block md:mb-2">{{ timeLeft ? timeLeft : 'Played' }}</span>
<div class="px-4 flex-1 md:flex-grow-0 md:w-full">
2024-05-19 05:49:42 +00:00
<EpisodeProgress v-if="shouldShowProgress" :episode="episode" :position="currentPosition" />
</div>
</div>
</a>
</template>
<script setup lang="ts">
import DOMPurify from 'dompurify'
2024-06-02 07:48:18 +00:00
import { orderBy } from 'lodash'
import { faBookmark, faPause, faPlay } from '@fortawesome/free-solid-svg-icons'
import { computed, defineAsyncComponent, toRefs } from 'vue'
import { eventBus, secondsToHumanReadable } from '@/utils'
2024-05-19 05:49:42 +00:00
import { useDraggable } from '@/composables'
import { formatTimeAgo } from '@vueuse/core'
import { playbackService } from '@/services'
import { songStore as episodeStore, preferenceStore as preferences, queueStore } from '@/stores'
const props = defineProps<{ episode: Episode, podcast: Podcast }>()
2024-06-02 07:48:18 +00:00
const EpisodeProgress = defineAsyncComponent(() => import('@/components/podcast/EpisodeProgress.vue'))
2024-05-19 05:49:42 +00:00
const { episode, podcast } = toRefs(props)
const { startDragging } = useDraggable('playables')
const publicationDateForHumans = computed(() => {
const publishedAt = new Date(episode.value.created_at)
if ((Date.now() - publishedAt.getTime()) / (1000 * 60 * 60 * 24) < 31) {
return formatTimeAgo(publishedAt)
}
return publishedAt.toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
2024-05-19 05:49:42 +00:00
})
})
const currentPosition = computed(() => podcast.value.state.progresses[episode.value.id] || 0)
const timeLeft = computed(() => {
if (currentPosition.value === 0) {
return secondsToHumanReadable(episode.value.length)
}
2024-05-19 05:49:42 +00:00
const secondsLeft = episode.value.length - currentPosition.value
return secondsLeft === 0 ? 0 : `${secondsToHumanReadable(secondsLeft)} left`
2024-05-19 05:49:42 +00:00
})
const shouldShowProgress = computed(() => timeLeft.value !== 0 && episode.value.length && currentPosition.value)
const isCurrentEpisode = computed(() => podcast.value.state.current_episode === episode.value.id)
const description = computed(() => DOMPurify.sanitize(episode.value.episode_description))
const onDragStart = (event: DragEvent) => startDragging(event, episode.value)
const requestContextMenu = (event: MouseEvent) => eventBus.emit('PLAYABLE_CONTEXT_MENU_REQUESTED', event, episode.value)
const isPlaying = computed(() => episode.value.playback_state === 'Playing')
const playOrPause = async () => {
if (isPlaying.value) {
return playbackService.pause()
}
if (episode.value.playback_state === 'Paused') {
return playbackService.resume()
}
if (preferences.continuous_playback) {
queueStore.replaceQueueWith(orderBy(await episodeStore.fetchForPodcast(podcast.value.id), 'created_at'))
}
2024-06-07 12:53:24 +00:00
await playbackService.play(episode.value, currentPosition.value >= episode.value.length ? 0 : currentPosition.value)
2024-05-19 05:49:42 +00:00
}
</script>
<style scoped lang="postcss">
.description {
:deep(p) {
@apply mb-3;
}
:deep(a) {
@apply text-k-text-primary hover:text-k-accent;
}
}
</style>