188 lines
7.4 KiB
Svelte
188 lines
7.4 KiB
Svelte
|
|
<script>
|
||
|
|
export let currentlyPlaying
|
||
|
|
export let playlistItems
|
||
|
|
|
||
|
|
import { buildAudioEndpoint } from '$lib/audio-manager.js'
|
||
|
|
import { onMount } from 'svelte'
|
||
|
|
import { createEventDispatcher } from 'svelte'
|
||
|
|
import { generateURL } from '$lib/Jellyfin-api.js'
|
||
|
|
import { fade } from 'svelte/transition'
|
||
|
|
|
||
|
|
import ListItem from '$lib/listItem.svelte'
|
||
|
|
import MediaControl from '$lib/mediaControl.svelte'
|
||
|
|
|
||
|
|
const dispatch = createEventDispatcher()
|
||
|
|
|
||
|
|
$: currentlyPlayingImageId = 'Primary' in currentlyPlaying.ImageTags ? currentlyPlaying.Id : currentlyPlaying.AlbumId
|
||
|
|
$: currentlyPlayingImage = generateURL({
|
||
|
|
type: 'Image',
|
||
|
|
pathParams: { id: currentlyPlayingImageId },
|
||
|
|
})
|
||
|
|
|
||
|
|
$: audioEndpoint = buildAudioEndpoint(currentlyPlaying.Id, 'default')
|
||
|
|
let audio
|
||
|
|
let audioVolume = 0.1
|
||
|
|
let progressBar
|
||
|
|
|
||
|
|
let playingState = 'paused'
|
||
|
|
|
||
|
|
onMount(() => {
|
||
|
|
audio = document.getElementById('audio')
|
||
|
|
audio.volume = audioVolume
|
||
|
|
progressBar = document.getElementById('progress-bar')
|
||
|
|
|
||
|
|
if ('mediaSession' in navigator) {
|
||
|
|
navigator.mediaSession.setActionHandler('play', () => playSong())
|
||
|
|
navigator.mediaSession.setActionHandler('pause', () => pauseSong())
|
||
|
|
navigator.mediaSession.setActionHandler('stop', () => closeMediaPlayer())
|
||
|
|
navigator.mediaSession.setActionHandler('nexttrack', () => playNext())
|
||
|
|
navigator.mediaSession.setActionHandler('previoustrack', () => playPrevious())
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
$: updateAudioSrc(audioEndpoint)
|
||
|
|
const updateAudioSrc = (newAudioEndpoint) => {
|
||
|
|
if (!audio) {
|
||
|
|
return onMount(() => {
|
||
|
|
audio.src = newAudioEndpoint
|
||
|
|
playSong()
|
||
|
|
})
|
||
|
|
}
|
||
|
|
audio.src = newAudioEndpoint
|
||
|
|
playSong()
|
||
|
|
}
|
||
|
|
|
||
|
|
$: updateMediaSession(currentlyPlaying)
|
||
|
|
const updateMediaSession = (media) => {
|
||
|
|
if ('mediaSession' in navigator) {
|
||
|
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||
|
|
title: media.Name,
|
||
|
|
artist: media.Artists.join(' / '),
|
||
|
|
album: media.Album,
|
||
|
|
artwork: [
|
||
|
|
{
|
||
|
|
src: currentlyPlayingImage + '&width=96&height=96',
|
||
|
|
sizes: '96x96',
|
||
|
|
type: 'image/png',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
src: currentlyPlayingImage + '&width=128&height=128',
|
||
|
|
sizes: '128x128',
|
||
|
|
type: 'image/png',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
src: currentlyPlayingImage + '&width=192&height=192',
|
||
|
|
sizes: '192x192',
|
||
|
|
type: 'image/png',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
src: currentlyPlayingImage + '&width=256&height=256',
|
||
|
|
sizes: '256x256',
|
||
|
|
type: 'image/png',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
src: currentlyPlayingImage + '&width=384&height=384',
|
||
|
|
sizes: '384x384',
|
||
|
|
type: 'image/png',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
src: currentlyPlayingImage + '&width=512&height=512',
|
||
|
|
sizes: '512x512',
|
||
|
|
type: 'image/png',
|
||
|
|
},
|
||
|
|
],
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const playSong = () => {
|
||
|
|
audio.play()
|
||
|
|
playingState = 'playing'
|
||
|
|
}
|
||
|
|
|
||
|
|
const pauseSong = () => {
|
||
|
|
audio.pause()
|
||
|
|
playingState = 'paused'
|
||
|
|
}
|
||
|
|
|
||
|
|
const playNext = () => {
|
||
|
|
let nextSong = playlistItems[playlistItems.indexOf(currentlyPlaying) + 1]
|
||
|
|
if (nextSong) {
|
||
|
|
dispatch('startPlayback', {
|
||
|
|
item: nextSong,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const playPrevious = () => {
|
||
|
|
let previousSong = playlistItems[playlistItems.indexOf(currentlyPlaying) - 1]
|
||
|
|
if (previousSong) {
|
||
|
|
dispatch('startPlayback', {
|
||
|
|
item: previousSong,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const updateProgressBar = (event) => {
|
||
|
|
if (event.target.currentTime) {
|
||
|
|
let currentPercentage = event.target.currentTime / event.target.duration
|
||
|
|
if (document.activeElement !== progressBar) {
|
||
|
|
progressBar.value = currentPercentage
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const updateAudioTime = () => {
|
||
|
|
let audioTimeStamp = audio.duration * progressBar.value
|
||
|
|
audio.currentTime = audioTimeStamp
|
||
|
|
}
|
||
|
|
|
||
|
|
const closeMediaPlayer = () => {
|
||
|
|
dispatch('closeMediaPlayer')
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<div id="layout" class="grid h-full grid-cols-1 grid-rows-[3fr_2fr] lg:grid-cols-[2fr_1fr] lg:grid-rows-1">
|
||
|
|
<div class="relative h-full overflow-hidden">
|
||
|
|
<div class="absolute z-0 flex h-full w-full items-center justify-items-center bg-neutral-900">
|
||
|
|
<div class="absolute z-10 h-full w-full backdrop-blur-3xl"></div>
|
||
|
|
{#key currentlyPlaying}
|
||
|
|
<img in:fade src={currentlyPlayingImage} alt="" class="absolute h-full w-full object-cover brightness-[70%]" />
|
||
|
|
{/key}
|
||
|
|
</div>
|
||
|
|
<div class="absolute grid h-full w-full grid-rows-[auto_8rem_3rem_6rem] justify-items-center p-8">
|
||
|
|
{#key currentlyPlaying}
|
||
|
|
<img in:fade src={currentlyPlayingImage} alt="" class="h-full min-h-[8rem] overflow-hidden rounded-xl object-contain p-2" />
|
||
|
|
{/key}
|
||
|
|
<div in:fade class="flex flex-col items-center justify-center gap-1 px-8 text-center font-notoSans">
|
||
|
|
{#key currentlyPlaying}
|
||
|
|
<span class="text-xl text-neutral-500">{currentlyPlaying.Album}</span>
|
||
|
|
<span class="text-3xl text-neutral-300">{currentlyPlaying.Name}</span>
|
||
|
|
<span class="text-xl text-neutral-500">{currentlyPlaying.Artists.join(' / ')}</span>
|
||
|
|
{/key}
|
||
|
|
</div>
|
||
|
|
<input id="progress-bar" on:mouseup={updateAudioTime} type="range" value="0" min="0" max="1" step="any" class="w-[90%] cursor-pointer rounded-lg bg-gray-400" />
|
||
|
|
<div class="flex h-full w-11/12 justify-around overflow-hidden">
|
||
|
|
<MediaControl type={'previoustrack'} on:mediaControlEvent={() => playPrevious()} />
|
||
|
|
<MediaControl type={playingState} on:mediaControlEvent={(event) => (event.detail.eventType === 'playing' ? pauseSong() : playSong())} />
|
||
|
|
<MediaControl type={'stop'} on:mediaControlEvent={() => closeMediaPlayer()} />
|
||
|
|
<MediaControl type={'nexttrack'} on:mediaControlEvent={() => playNext()} />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="no-scrollbar flex w-full flex-col items-center divide-y-[1px] divide-[#353535] overflow-y-scroll bg-neutral-900 p-4">
|
||
|
|
{#each playlistItems as item}
|
||
|
|
{#if item == currentlyPlaying}
|
||
|
|
<div class="flex w-full bg-neutral-500">
|
||
|
|
<ListItem {item} />
|
||
|
|
</div>
|
||
|
|
{:else}
|
||
|
|
<div class="flex w-full hover:bg-neutral-500">
|
||
|
|
<ListItem {item} on:startPlayback />
|
||
|
|
</div>
|
||
|
|
{/if}
|
||
|
|
{/each}
|
||
|
|
</div>
|
||
|
|
<audio id="audio" on:ended={playNext} on:timeupdate={updateProgressBar} crossorigin="anonymous" class="hidden" />
|
||
|
|
</div>
|