Added fullscreen mode to miniplayer

This commit is contained in:
Eclypsed
2024-04-22 14:18:42 -04:00
parent 2ee60ef302
commit 28e4569507
9 changed files with 391 additions and 166 deletions

View File

@@ -5,6 +5,8 @@
import { goto } from '$app/navigation'
import { queue } from '$lib/stores'
let queueRef = $queue // This nonsense is to prevent an bug that causes svelte to throw an error when setting a property of the queue directly
let image: HTMLImageElement, captionText: HTMLDivElement
</script>
@@ -28,7 +30,7 @@
<IconButton
halo={true}
on:click={() => {
if (mediaItem.type === 'song') $queue.enqueue(mediaItem)
if (mediaItem.type === 'song') queueRef.current = mediaItem
}}
>
<i slot="icon" class="fa-solid fa-play text-xl" />

View File

@@ -5,7 +5,9 @@
// import { FastAverageColor } from 'fast-average-color'
import Slider from '$lib/components/util/slider.svelte'
$: currentlyPlaying = $queue.current()
$: currentlyPlaying = $queue.current
let expanded = false
let paused = true,
shuffle = false,
@@ -14,7 +16,8 @@
let volume: number,
muted = false
$: if (volume) localStorage.setItem('volume', volume.toString())
$: muted ? (volume = 0) : (volume = Number(localStorage.getItem('volume')))
$: if (volume && !muted) localStorage.setItem('volume', volume.toString())
const formatTime = (seconds: number): string => {
seconds = Math.floor(seconds)
@@ -25,6 +28,18 @@
return hours > 0 ? `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` : `${minutes}:${seconds.toString().padStart(2, '0')}`
}
$: if (currentlyPlaying) updateMediaSession(currentlyPlaying)
const updateMediaSession = (media: Song) => {
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: media.name,
artist: media.artists?.map((artist) => artist.name).join(', ') || media.createdBy?.name,
album: media.album?.name,
artwork: [{ src: `/api/remoteImage?url=${media.thumbnail}`, sizes: '256x256', type: 'image/png' }],
})
}
}
onMount(() => {
const storedVolume = localStorage.getItem('volume')
if (storedVolume) {
@@ -33,6 +48,14 @@
localStorage.setItem('volume', '0.5')
volume = 0.5
}
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('play', () => (paused = false))
navigator.mediaSession.setActionHandler('pause', () => (paused = true))
navigator.mediaSession.setActionHandler('stop', () => $queue.clear())
navigator.mediaSession.setActionHandler('nexttrack', () => $queue.next())
navigator.mediaSession.setActionHandler('previoustrack', () => $queue.previous())
}
})
let currentTime: number = 0
@@ -42,84 +65,211 @@
let progressBar: Slider
let durationTimestamp: HTMLSpanElement
let expandedCurrentTimeTimestamp: HTMLSpanElement
let expandedProgressBar: Slider
let expandedDurationTimestamp: HTMLSpanElement
let seeking: boolean = false
$: if (!seeking && currentTimeTimestamp) currentTimeTimestamp.innerText = formatTime(currentTime)
$: if (!seeking && progressBar) progressBar.$set({ value: currentTime })
$: if (!seeking && durationTimestamp) durationTimestamp.innerText = formatTime(duration)
$: if (!seeking && expandedCurrentTimeTimestamp) expandedCurrentTimeTimestamp.innerText = formatTime(currentTime)
$: if (!seeking && expandedProgressBar) expandedProgressBar.$set({ value: currentTime })
$: if (!seeking && expandedDurationTimestamp) expandedDurationTimestamp.innerText = formatTime(duration)
</script>
{#if currentlyPlaying}
<main transition:slide class="relative m-4 grid h-20 grid-cols-[minmax(auto,_20rem)_auto_minmax(auto,_20rem)] gap-4 overflow-clip rounded-xl bg-neutral-925 text-white transition-colors duration-1000">
<section class="flex gap-3">
<div class="relative h-full w-20 min-w-20">
{#key currentlyPlaying}
<div transition:fade={{ duration: 500 }} class="absolute h-full w-full bg-cover bg-center bg-no-repeat" style="background-image: url(/api/remoteImage?url={currentlyPlaying.thumbnail});" />
{/key}
</div>
<section class="flex flex-col justify-center gap-1">
<div class="line-clamp-2 text-sm">{currentlyPlaying.name}</div>
<div class="text-xs">{currentlyPlaying.artists?.map((artist) => artist.name).join(', ') || currentlyPlaying.createdBy?.name}</div>
</section>
</section>
<section class="flex min-w-max flex-col items-center justify-center gap-1">
<div class="flex items-center gap-3 text-lg">
<button on:click={() => (shuffle = !shuffle)} class="aspect-square h-8">
<i class="fa-solid fa-shuffle" />
</button>
<button class="aspect-square h-8">
<i class="fa-solid fa-backward-step" />
</button>
<button on:click={() => (paused = !paused)} class="grid aspect-square h-8 place-items-center rounded-full bg-white">
<i class="fa-solid {paused ? 'fa-play' : 'fa-pause'} text-black" />
</button>
<button class="aspect-square h-8">
<i class="fa-solid fa-forward-step" />
</button>
<button on:click={() => (repeat = !repeat)} class="aspect-square h-8">
<i class="fa-solid fa-repeat" />
</button>
</div>
<div class="flex items-center justify-items-center gap-2">
<span bind:this={currentTimeTimestamp} class="w-16 text-right" />
<div class="w-72">
<Slider
bind:this={progressBar}
max={duration}
on:seeking={(event) => {
currentTimeTimestamp.innerText = formatTime(event.detail.value)
seeking = true
}}
on:seeked={(event) => {
currentTime = event.detail.value
seeking = false
}}
/>
</div>
<span bind:this={durationTimestamp} class="w-16 text-left" />
</div>
</section>
<section class="flex items-center justify-end pr-2 text-lg">
<div id="volume-slider" class="flex h-10 w-fit flex-shrink-0 flex-row-reverse items-center gap-2">
<button on:click={() => (muted = !muted)} class="aspect-square h-8">
<i class="fa-solid {volume > 0.5 ? 'fa-volume-high' : volume > 0 ? 'fa-volume-low' : 'fa-volume-xmark'} w-full text-center text-base" />
</button>
<div id="slider-wrapper" class="w-0 transition-all duration-500">
<Slider bind:value={volume} max={1} />
</div>
</div>
<button class="aspect-square h-8" on:click={() => console.log('close')}>
<i class="fa-solid fa-xmark" />
</button>
</section>
<div transition:slide class="{expanded ? 'h-full w-full' : 'm-4 h-20 w-[calc(100%_-_32px)] rounded-xl'} absolute bottom-0 z-40 overflow-clip bg-neutral-925 transition-all duration-500">
{#if !expanded}
<main in:fade={{ duration: 75, delay: 500 }} out:fade={{ duration: 75 }} class="relative grid h-20 w-full grid-cols-[minmax(auto,_20rem)_auto_minmax(auto,_20rem)] gap-4">
<section class="flex gap-3">
<div class="relative h-full w-20 min-w-20">
{#key currentlyPlaying}
<div transition:fade={{ duration: 500 }} class="absolute h-full w-full bg-cover bg-center bg-no-repeat" style="background-image: url(/api/remoteImage?url={currentlyPlaying.thumbnail});" />
{/key}
</div>
<section class="flex flex-col justify-center gap-1">
<div class="line-clamp-2 text-sm">{currentlyPlaying.name}</div>
<div class="text-xs">{currentlyPlaying.artists?.map((artist) => artist.name).join(', ') || currentlyPlaying.createdBy?.name}</div>
</section>
</section>
<section class="flex min-w-max flex-col items-center justify-center gap-1">
<div class="flex items-center gap-3 text-lg">
<button on:click={() => (shuffle = !shuffle)} class="aspect-square h-8">
<i class="fa-solid fa-shuffle" />
</button>
<button class="aspect-square h-8" on:click={() => $queue.previous()}>
<i class="fa-solid fa-backward-step" />
</button>
<button on:click={() => (paused = !paused)} class="grid aspect-square h-8 place-items-center rounded-full bg-white">
<i class="fa-solid {paused ? 'fa-play' : 'fa-pause'} text-black" />
</button>
<button class="aspect-square h-8" on:click={() => $queue.next()}>
<i class="fa-solid fa-forward-step" />
</button>
<button on:click={() => (repeat = !repeat)} class="aspect-square h-8">
<i class="fa-solid fa-repeat" />
</button>
</div>
<div class="flex items-center justify-items-center gap-2">
<span bind:this={currentTimeTimestamp} class="w-16 text-right" />
<div class="w-72">
<Slider
bind:this={progressBar}
max={duration}
on:seeking={(event) => {
currentTimeTimestamp.innerText = formatTime(event.detail.value)
seeking = true
}}
on:seeked={(event) => {
currentTime = event.detail.value
seeking = false
}}
/>
</div>
<span bind:this={durationTimestamp} class="w-16 text-left" />
</div>
</section>
<section class="flex items-center justify-end gap-2 pr-2 text-lg">
<div id="volume-slider" class="flex h-10 flex-row-reverse items-center gap-2">
<button on:click={() => (muted = !muted)} class="aspect-square h-8">
<i class="fa-solid {volume > 0.5 ? 'fa-volume-high' : volume > 0 ? 'fa-volume-low' : 'fa-volume-xmark'} w-full text-center" />
</button>
<div id="slider-wrapper" class="w-24 transition-all duration-500">
<Slider bind:value={volume} max={1} />
</div>
</div>
<button class="aspect-square h-8" on:click={() => (expanded = true)}>
<i class="fa-solid fa-expand" />
</button>
<button class="aspect-square h-8" on:click={() => $queue.clear()}>
<i class="fa-solid fa-xmark" />
</button>
</section>
</main>
{:else}
<main in:fade={{ delay: 500 }} out:fade={{ duration: 75 }} class="expanded-player h-full" style="--currentlyPlayingImage: url(/api/remoteImage?url={currentlyPlaying.thumbnail});">
<section class="song-queue-wrapper h-full px-24 py-20">
<section class="relative">
{#key currentlyPlaying}
<img transition:fade={{ duration: 300 }} class="absolute h-full max-w-full object-contain py-8" src="/api/remoteImage?url={currentlyPlaying.thumbnail}" alt="" />
{/key}
</section>
<section class="flex flex-col gap-2">
<div class="ml-2 text-2xl">Up next</div>
{#each $queue.list as item, index}
{@const isCurrent = item === currentlyPlaying}
<button
on:click={() => {
if (!isCurrent) $queue.current = item
}}
class="queue-item w-full items-center gap-3 rounded-xl p-3 {isCurrent ? 'bg-[rgba(64,_64,_64,_0.5)]' : 'bg-[rgba(10,_10,_10,_0.5)]'}"
>
<div class="justify-self-center">{index + 1}</div>
<img class="justify-self-center" src="/api/remoteImage?url={item.thumbnail}" alt="" draggable="false" />
<div class="justify-items-left text-left">
<div class="line-clamp-2">{item.name}</div>
<div class="mt-[.15rem] text-neutral-500">{currentlyPlaying.artists?.map((artist) => artist.name).join(', ') || currentlyPlaying.createdBy?.name}</div>
</div>
<span class="text-right">{item.duration ?? ''}</span>
</button>
{/each}
</section>
</section>
<section class="px-8">
<div class="progress-bar-expanded mb-8">
<span bind:this={expandedCurrentTimeTimestamp} class="text-right" />
<Slider
bind:this={expandedProgressBar}
max={duration}
on:seeking={(event) => {
expandedCurrentTimeTimestamp.innerText = formatTime(event.detail.value)
seeking = true
}}
on:seeked={(event) => {
currentTime = event.detail.value
seeking = false
}}
/>
<span bind:this={expandedDurationTimestamp} class="text-left" />
</div>
<div class="expanded-controls">
<div>
<div class="mb-2 line-clamp-2 text-3xl">{currentlyPlaying.name}</div>
<div class="line-clamp-1 text-lg">
{currentlyPlaying.artists?.map((artist) => artist.name).join(', ') || currentlyPlaying.createdBy?.name}{currentlyPlaying.album ? ` - ${currentlyPlaying.album.name}` : ''}
</div>
</div>
<div class="flex w-full items-center justify-center gap-2 text-2xl">
<button on:click={() => (shuffle = !shuffle)} class="aspect-square h-16">
<i class="fa-solid fa-shuffle" />
</button>
<button class="aspect-square h-16" on:click={() => $queue.previous()}>
<i class="fa-solid fa-backward-step" />
</button>
<button on:click={() => (paused = !paused)} class="grid aspect-square h-16 place-items-center rounded-full bg-white">
<i class="fa-solid {paused ? 'fa-play' : 'fa-pause'} text-black" />
</button>
<button class="aspect-square h-16" on:click={() => $queue.next()}>
<i class="fa-solid fa-forward-step" />
</button>
<button on:click={() => (repeat = !repeat)} class="aspect-square h-16">
<i class="fa-solid fa-repeat" />
</button>
</div>
<section class="flex items-center justify-end gap-2 text-xl">
<div id="volume-slider" class="flex h-10 flex-row-reverse items-center gap-2">
<button on:click={() => (muted = !muted)} class="aspect-square h-8">
<i class="fa-solid {volume > 0.5 ? 'fa-volume-high' : volume > 0 ? 'fa-volume-low' : 'fa-volume-xmark'} w-full text-center" />
</button>
<div id="slider-wrapper" class="w-24 transition-all duration-500">
<Slider bind:value={volume} max={1} />
</div>
</div>
<button class="aspect-square h-8" on:click={() => (expanded = false)}>
<i class="fa-solid fa-compress" />
</button>
<button class="aspect-square h-8" on:click={() => $queue.clear()}>
<i class="fa-solid fa-xmark" />
</button>
</section>
</div>
</section>
</main>
{/if}
<audio autoplay bind:paused bind:volume bind:currentTime bind:duration on:ended={() => $queue.next()} src="/api/audio?connection={currentlyPlaying.connection}&id={currentlyPlaying.id}" />
</main>
</div>
{/if}
<style>
#volume-slider:hover > #slider-wrapper {
width: 6rem;
.expanded-player {
display: grid;
grid-template-rows: auto 12rem;
/* background: linear-gradient(to left, rgba(16, 16, 16, 0.9), rgb(16, 16, 16)), var(--currentlyPlayingImage); */
background-repeat: no-repeat !important;
background-size: cover !important;
background-position: center !important;
}
#slider-wrapper:focus-within {
width: 6rem;
.song-queue-wrapper {
display: grid;
grid-template-columns: 3fr 2fr;
gap: 4rem;
}
.queue-item {
display: grid;
grid-template-columns: 1rem 50px auto min-content;
}
.queue-item:hover {
background-color: rgba(64, 64, 64, 0.5);
}
.progress-bar-expanded {
display: grid;
grid-template-columns: min-content auto min-content;
align-items: center;
gap: 1rem;
}
.expanded-controls {
display: grid;
grid-template-columns: 1fr min-content 1fr;
}
</style>