mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: adapt remote controller to Podcast
This commit is contained in:
parent
0f67ce2478
commit
f14cbcd00a
7 changed files with 82 additions and 73 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
36
resources/assets/js/remote/components/PlayableDetails.vue
Normal file
36
resources/assets/js/remote/components/PlayableDetails.vue
Normal 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>
|
|
@ -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')
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -1,4 +1,4 @@
|
|||
export interface RemoteState {
|
||||
song: Playable | null
|
||||
playable: Playable | null
|
||||
volume: number
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue