feat: adapt remote controller to Podcast

This commit is contained in:
Phan An 2024-06-03 13:22:16 +08:00
parent 0f67ce2478
commit f14cbcd00a
7 changed files with 82 additions and 73 deletions

View file

@ -52,9 +52,9 @@ export interface Events {
SOCKET_PLAY_PREV: () => void
SOCKET_PLAYBACK_STOPPED: () => void
SOCKET_GET_STATUS: () => void
SOCKET_STATUS: (data: { song?: Playable, volume: number }) => void
SOCKET_GET_CURRENT_SONG: () => void
SOCKET_SONG: (song: Playable) => void
SOCKET_STATUS: (data: { playable?: Playable, volume: number }) => void
SOCKET_GET_CURRENT_PLAYABLE: () => void
SOCKET_PLAYABLE: (playable: Playable) => void
SOCKET_SET_VOLUME: (volume: number) => void
SOCKET_VOLUME_CHANGED: (volume: number) => void
}

View file

@ -1,13 +1,13 @@
<template>
<div :class="{ 'standalone' : inStandaloneMode }" class="h-screen bg-k-bg-primary">
<template v-if="authenticated">
<AlbumArtOverlay v-if="showAlbumArtOverlay && state.song && isSong(state.song)" :album="state.song.album_id" />
<AlbumArtOverlay v-if="showAlbumArtOverlay" :album="(state.playable as Song).album_id" />
<main class="h-screen flex flex-col items-center justify-between text-center relative z-[1]">
<template v-if="connected">
<template v-if="state.song">
<SongDetails :song="state.song" />
<RemoteFooter :song="state.song" />
<template v-if="state.playable">
<PlayableDetails :playable="state.playable" />
<RemoteFooter :playable="state.playable" />
</template>
<p v-else class="text-k-text-secondary">No song is playing.</p>
@ -25,11 +25,11 @@
<script lang="ts" setup>
import { authService, socketService } from '@/services'
import { preferenceStore, userStore } from '@/stores'
import { defineAsyncComponent, onMounted, provide, reactive, ref, toRef } from 'vue'
import { computed, defineAsyncComponent, onMounted, provide, reactive, ref } from 'vue'
import { isSong, logger } from '@/utils'
import { RemoteState } from '@/remote/types'
import type { RemoteState } from '@/remote/types'
const SongDetails = defineAsyncComponent(() => import('@/remote/components/SongDetails.vue'))
const PlayableDetails = defineAsyncComponent(() => import('@/remote/components/PlayableDetails.vue'))
const Scanner = defineAsyncComponent(() => import('@/remote/components/Scanner.vue'))
const RemoteFooter = defineAsyncComponent(() => import('@/remote/components/RemoteFooter.vue'))
const AlbumArtOverlay = defineAsyncComponent(() => import('@/components/ui/AlbumArtOverlay.vue'))
@ -37,7 +37,12 @@ const LoginForm = defineAsyncComponent(() => import('@/components/auth/LoginForm
const authenticated = ref(false)
const connected = ref(false)
const showAlbumArtOverlay = toRef(preferenceStore.state, 'show_album_art_overlay')
const showAlbumArtOverlay = computed(() => {
return preferenceStore.show_album_art_overlay
&& state.playable
&& isSong(state.playable)
})
const inStandaloneMode = ref(
(window.navigator as any).standalone || window.matchMedia('(display-mode: standalone)').matches
@ -49,8 +54,8 @@ const onUserLoggedIn = async () => {
}
const state = reactive<RemoteState>({
volume: 0,
song: null as Playable | null
playable: null,
volume: 0
})
provide('state', state)
@ -61,12 +66,12 @@ const init = async () => {
await socketService.init()
socketService
.listen('SOCKET_SONG', song => (state.song = song))
.listen('SOCKET_PLAYBACK_STOPPED', () => state.song && (state.song.playback_state = 'Stopped'))
.listen('SOCKET_PLAYABLE', playable => (state.playable = playable))
.listen('SOCKET_PLAYBACK_STOPPED', () => state.playable && (state.playable.playback_state = 'Stopped'))
.listen('SOCKET_VOLUME_CHANGED', (volume: number) => state.volume = volume)
.listen('SOCKET_STATUS', (data: { song?: Playable, volume: number }) => {
.listen('SOCKET_STATUS', (data: { playable?: Playable, volume: number }) => {
state.volume = data.volume || 0
state.song = data.song || null
state.playable = data.playable || null
connected.value = true
})
} catch (error: unknown) {

View file

@ -0,0 +1,36 @@
<template>
<article class="flex-1 flex flex-col items-center justify-around w-screen">
<img
:src="coverArt"
class="my-0 mx-auto w-[calc(70vw_+_4px)] aspect-square rounded-full border-2 border-solid border-k-text-primary object-center object-cover"
alt="Cover art"
/>
<div class="w-full flex flex-col justify-around px-6">
<div>
<p class="text-[6vmin] font-bold mx-auto mb-4">{{ playable.title }}</p>
<p class="text-[5vmin] mb-2 opacity-50">{{ artist }}</p>
<p class="text-[4vmin] opacity-50">{{ album }}</p>
</div>
</div>
</article>
</template>
<script lang="ts" setup>
import { computed, toRefs } from 'vue'
import { defaultCover, getPlayableProp } from '@/utils'
const props = defineProps<{ playable: Playable }>()
const { playable } = toRefs(props)
const coverArt = computed(() => getPlayableProp<string>(playable.value, 'album_cover', 'episode_image') || defaultCover)
const artist = computed(() => getPlayableProp(playable.value, 'artist_name', 'podcast_author'))
const album = computed(() => getPlayableProp(playable.value, 'album_name', 'podcast_title'))
</script>
<style lang="postcss" scoped>
p {
@apply max-w-[90%] mx-auto overflow-hidden text-ellipsis whitespace-nowrap leading-[1.3];
}
</style>

View file

@ -1,28 +1,31 @@
<template>
<footer
class="h-[18vh] w-screen flex justify-around items-center border-t border-solid border-t-white/10 py-4 text-[5vmin]"
>
<a class="has-[.yep]:text-k-love" @click.prevent="toggleFavorite">
<Icon :class="song.liked && 'yep'" :icon="song.liked ? faHeart : faEmptyHeart" />
</a>
<footer class="h-[18vh] w-screen flex justify-around items-center border-t border-solid border-t-white/10 py-4">
<button
class="text-[5vmin] has-[.yep]:text-k-love"
data-testid="btn-toggle-like"
@click.prevent="toggleFavorite"
>
<Icon :class="playable.liked && 'yep'" :icon="playable.liked ? faHeart : faEmptyHeart" />
</button>
<a class="text-[6vmin]" @click="playPrev">
<button class="text-[6vmin]" data-testid="btn-play-prev" @click.prevent="playPrev">
<Icon :icon="faStepBackward" />
</a>
</button>
<a
<button
class="text-[7vmin] w-[16vmin] aspect-square border border-solid border-k-text-primary rounded-full flex
items-center justify-center has-[.paused]:pl-[4px]"
data-testid="btn-toggle-playback"
@click.prevent="togglePlayback"
>
<Icon :class="playing || 'paused'" :icon="playing ? faPause : faPlay" />
</a>
</button>
<a class="text-[6vmin]" @click.prevent="playNext">
<button class="text-[6vmin]" data-testid="btn-play-next" @click.prevent="playNext">
<Icon :icon="faStepForward" />
</a>
</button>
<VolumeControl />
<VolumeControl class="text-[5vmin]" />
</footer>
</template>
@ -34,18 +37,18 @@ import { socketService } from '@/services'
import VolumeControl from '@/remote/components/VolumeControl.vue'
const props = defineProps<{ song: Playable }>()
const { song } = toRefs(props)
const props = defineProps<{ playable: Playable }>()
const { playable } = toRefs(props)
const toggleFavorite = () => {
song.value.liked = !song.value.liked
playable.value.liked = !playable.value.liked
socketService.broadcast('SOCKET_TOGGLE_FAVORITE')
}
const playing = computed(() => song.value.playback_state === 'Playing')
const playing = computed(() => playable.value.playback_state === 'Playing')
const togglePlayback = () => {
song.value.playback_state = song.value.playback_state === 'Playing' ? 'Paused' : 'Playing'
playable.value.playback_state = playable.value.playback_state === 'Playing' ? 'Paused' : 'Playing'
socketService.broadcast('SOCKET_TOGGLE_PLAYBACK')
}

View file

@ -1,35 +0,0 @@
<template>
<article class="flex-1 flex flex-col items-center justify-around">
<div
:style="{ backgroundImage: `url(${image || defaultCover})` }"
class="cover my-0 mx-auto w-[calc(70vw_+_4px)] aspect-square rounded-full border-2 border-solid
border-k-text-primary bg-center bg-cover bg-k-bg-secondary"
/>
<div class="w-full flex flex-col justify-around">
<div>
<p class="text-[6vmin] font-bold mx-auto mb-4">{{ song.title }}</p>
<p class="text-[5vmin] mb-2 opacity-50">{{ artist }}</p>
<p class="text-[4vmin] opacity-50">{{ album }}</p>
</div>
</div>
</article>
</template>
<script lang="ts" setup>
import { computed, toRefs } from 'vue'
import { defaultCover, getPlayableProp } from '@/utils'
const props = defineProps<{ song: Playable }>()
const { song } = toRefs(props)
const image = computed(() => getPlayableProp(song.value, 'album_cover', 'episode_image'))
const artist = computed(() => getPlayableProp(song.value, 'artist_name', 'podcast_author'))
const album = computed(() => getPlayableProp(song.value, 'album_name', 'podcast_title'))
</script>
<style lang="postcss" scoped>
p {
@apply max-w-[90%] mx-auto overflow-hidden text-ellipsis whitespace-nowrap leading-[1.3];
}
</style>

View file

@ -1,4 +1,4 @@
export interface RemoteState {
song: Playable | null
playable: Playable | null
volume: number
}

View file

@ -9,11 +9,11 @@ export const socketListener = {
.listen('SOCKET_PLAY_PREV', () => playbackService.playPrev())
.listen('SOCKET_GET_STATUS', () => {
socketService.broadcast('SOCKET_STATUS', {
song: queueStore.current,
playable: queueStore.current,
volume: volumeManager.get()
})
})
.listen('SOCKET_GET_CURRENT_SONG', () => socketService.broadcast('SOCKET_SONG', queueStore.current))
.listen('SOCKET_GET_CURRENT_PLAYABLE', () => socketService.broadcast('SOCKET_PLAYABLE', queueStore.current))
.listen('SOCKET_SET_VOLUME', (volume: number) => volumeManager.set(volume))
.listen('SOCKET_TOGGLE_FAVORITE', () => queueStore.current && favoriteStore.toggleOne(queueStore.current))
}