New Miniplaer + ScrollingText && ArtistList components

This commit is contained in:
Eclypsed
2024-06-11 03:55:34 -04:00
parent 9dab826e53
commit ca80a6476f
9 changed files with 272 additions and 222 deletions

View File

@@ -0,0 +1,42 @@
<!--
@component
A component to easily display the artists of a track or album, or the user associated with either a Song, Album, or Playlist
object. Formatting of the text such as font size, weight, and line clamps can be specified in a wrapper element.
@param mediaItem Either a Song, Album, or Playlist object.
@param linked Boolean. If true artists will be linked with anchor tags. Defaults to true.
-->
<script lang="ts">
export let mediaItem: Song | Album | Playlist
export let linked = true
</script>
<div class="break-keep">
{#if 'artists' in mediaItem && mediaItem.artists && typeof mediaItem.artists === 'string'}
{mediaItem.artists}
{:else if 'artists' in mediaItem && mediaItem.artists && typeof mediaItem.artists !== 'string' && mediaItem.artists.length > 0}
{#each mediaItem.artists as artist, index}
{#if linked}
<a class="hover:underline focus:underline" href="/details/artist?id={artist.id}&connection={mediaItem.connection.id}">{artist.name}</a>
{:else}
<span>{artist.name}</span>
{/if}
{#if index < mediaItem.artists.length - 1}
<span style="margin-left: -0.25em; margin-right: 0.25em">&#44;</span>
{/if}
{/each}
{:else if 'uploader' in mediaItem && mediaItem.uploader}
{#if linked}
<a class="hover:underline focus:underline" href="/details/user?id={mediaItem.uploader.id}&connection={mediaItem.connection.id}">{mediaItem.uploader.name}</a>
{:else}
<span>{mediaItem.uploader.name}</span>
{/if}
{:else if 'createdBy' in mediaItem && mediaItem.createdBy}
{#if linked}
<a class="hover:underline focus:underline" href="/details/user?id={mediaItem.createdBy.id}&connection={mediaItem.connection.id}">{mediaItem.createdBy.name}</a>
{:else}
<span>{mediaItem.createdBy.name}</span>
{/if}
{/if}
</div>

View File

@@ -7,6 +7,8 @@
@param thumbnailUrl A string of a URL that points to the desired image.
@param alt Supplementary text in the event the image fails to load.
@param loadingMethod Optional. Either the string 'lazy' or 'eager', defaults to lazy. The method by which to load the image.
@param objectFit One of the following fill, contain, cover, none, scale-down. Specifies the object-fit styling of the image
@param objectPosistion Optional. Specifies the object-position styling of the image. Defaults to 'center'
-->
<script lang="ts">
@@ -15,17 +17,11 @@
export let thumbnailUrl: string
export let alt: string
export let loadingMethod: 'lazy' | 'eager' = 'lazy'
export let objectFit: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
export let objectPosition: string = 'center'
let imageContainer: HTMLDivElement
function removeOldImage() {
if (imageContainer.childElementCount > 1) {
const oldImage = imageContainer.firstChild! as HTMLImageElement
oldImage.style.opacity = '0'
setTimeout(() => imageContainer.removeChild(oldImage), 500)
}
}
function updateImage(newThumbnailURL: string) {
if (!imageContainer) return
@@ -41,10 +37,19 @@
newImage.style.width = '100%'
newImage.style.height = '100%'
newImage.style.objectFit = 'cover'
newImage.style.objectFit = objectFit
newImage.style.objectPosition = objectPosition
newImage.style.opacity = '0'
newImage.style.position = 'absolute'
function removeOldImage() {
if (imageContainer.childElementCount > 1) {
const oldImage = imageContainer.firstChild! as HTMLImageElement
oldImage.style.opacity = '0'
setTimeout(() => imageContainer.removeChild(oldImage), 500)
}
}
newImage.onload = () => {
removeOldImage()
newImage.style.transition = 'opacity 500ms ease'

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import LazyImage from './lazyImage.svelte'
import ArtistList from './artistList.svelte'
export let mediaItem: Song | Album | Artist | Playlist
@@ -11,7 +12,7 @@
<div id="list-item" class="h-16 w-full">
<div class="h-full overflow-clip rounded-md">
{#if thumbnailUrl}
<LazyImage {thumbnailUrl} alt={`${mediaItem.name} thumbnial`} />
<LazyImage {thumbnailUrl} alt={`${mediaItem.name} thumbnial`} objectFit={'cover'} />
{:else}
<div id="thumbnail-placeholder" class="grid h-full w-full place-items-center bg-lazuli-primary">
<i class="fa-solid {mediaItem.type === 'artist' ? 'fa-user' : 'fa-play'} text-2xl" />
@@ -19,22 +20,9 @@
{/if}
</div>
<div class="line-clamp-1">{mediaItem.name}</div>
<span class="line-clamp-1 flex text-neutral-400">
{#if 'artists' in mediaItem && mediaItem.artists}
{#if mediaItem.artists === 'Various Artists'}
<span>Various Artists</span>
{:else}
{#each mediaItem.artists as artist, index}
<a class="hover:underline focus:underline" href="/details/artist?id={artist.id}&connection={mediaItem.connection.id}">{artist.name}</a>
{#if index < mediaItem.artists.length - 1}
&#44&#160
{/if}
{/each}
{/if}
{:else if 'uploader' in mediaItem && mediaItem.uploader}
<span>{mediaItem.uploader.name}</span>
{:else if 'createdBy' in mediaItem && mediaItem.createdBy}
<span>{mediaItem.createdBy.name}</span>
<span class="line-clamp-1 text-neutral-400">
{#if mediaItem.type !== 'artist'}
<ArtistList {mediaItem} />
{/if}
</span>
<div class="justify-self-center text-neutral-400">{date ?? ''}</div>

View File

@@ -5,8 +5,6 @@
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
async function setQueueItems(mediaItem: Album | Playlist) {
@@ -15,7 +13,7 @@
}).then((response) => response.json() as Promise<{ items: Song[] }>)
const items = itemsResponse.items
queueRef.setQueue(items)
$queue.setQueue(items)
}
</script>
@@ -41,7 +39,7 @@
on:click={() => {
switch (mediaItem.type) {
case 'song':
queueRef.setQueue([mediaItem])
$queue.setQueue([mediaItem])
break
case 'album':
case 'playlist':

View File

@@ -6,6 +6,9 @@
import Slider from '$lib/components/util/slider.svelte'
import Loader from '$lib/components/util/loader.svelte'
import LazyImage from './lazyImage.svelte'
import IconButton from '$lib/components/util/iconButton.svelte'
import ScrollingText from '$lib/components/util/scrollingText.svelte'
import ArtistList from './artistList.svelte'
$: currentlyPlaying = $queue.current
@@ -31,17 +34,29 @@
return hours > 0 ? `${hours}:`.concat(durationString) : durationString
}
$: if (currentlyPlaying) updateMediaSession(currentlyPlaying)
function updateMediaSession(media: Song) {
if ('mediaSession' in navigator) {
$: updateMediaSession(currentlyPlaying)
function updateMediaSession(media: Song | null) {
if (!('mediaSession' in navigator)) return
if (!media) {
navigator.mediaSession.metadata = null
return
}
navigator.mediaSession.metadata = new MediaMetadata({
title: media.name,
artist: media.artists?.map((artist) => artist.name).join(', ') || media.uploader?.name,
album: media.album?.name,
artwork: [{ src: `/api/remoteImage?url=${media.thumbnailUrl}`, sizes: '256x256', type: 'image/png' }],
artwork: [
{ src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=96`, sizes: '96x96', type: 'image/png' },
{ src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=128`, sizes: '128x128', type: 'image/png' },
{ src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=192`, sizes: '192x192', type: 'image/png' },
{ src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=256`, sizes: '256x256', type: 'image/png' },
{ src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=384`, sizes: '384x384', type: 'image/png' },
{ src: `/api/remoteImage?url=${media.thumbnailUrl}&maxWidth=512`, sizes: '512x512', type: 'image/png' },
],
})
}
}
onMount(() => {
const storedVolume = Number(localStorage.getItem('volume'))
@@ -80,53 +95,51 @@
$: if (!seeking && expandedProgressBar) expandedProgressBar.$set({ value: currentTime })
$: if (!seeking && expandedDurationTimestamp) expandedDurationTimestamp.innerText = formatTime(duration)
let slidingText: HTMLElement
let slidingTextWidth: number, slidingTextWrapperWidth: number
let scrollDirection: 1 | -1 = 1
$: scrollDistance = slidingTextWidth - slidingTextWrapperWidth
$: if (slidingText && scrollDistance > 0) slidingText.style.animationDuration = `${scrollDistance / 50}s`
let audioElement: HTMLAudioElement
</script>
{#if currentlyPlaying}
<div id="player-wrapper" 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">
<main in:fade={{ duration: 75, delay: 500 }} out:fade={{ duration: 75 }} class="flex h-20 w-full gap-10 pr-8">
<section class="flex w-80 gap-3">
<div class="relative h-full w-20 min-w-20">
<LazyImage thumbnailUrl={currentlyPlaying.thumbnailUrl} alt={`${currentlyPlaying.name} jacket`} loadingMethod={'eager'} />
<LazyImage thumbnailUrl={currentlyPlaying.thumbnailUrl} alt={`${currentlyPlaying.name} jacket`} objectFit={'cover'} />
</div>
<section class="flex flex-grow flex-col justify-center gap-1">
<div class="h-6">
<ScrollingText>
<div slot="text" class="line-clamp-1 font-medium">{currentlyPlaying.name}</div>
</ScrollingText>
</div>
<div class="line-clamp-1 text-xs font-extralight">
<ArtistList mediaItem={currentlyPlaying} />
</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.uploader?.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={() => (shuffled ? $queue.reorder() : $queue.shuffle())} class="aspect-square h-8">
<i class="fa-solid {shuffled ? 'fa-shuffle' : 'fa-right-left'}" />
</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="relative grid aspect-square h-8 place-items-center rounded-full bg-white text-black">
<section class="flex flex-grow items-center gap-1 py-4">
<IconButton on:click={() => $queue.previous()}>
<i slot="icon" class="fa-solid fa-backward-step text-xl" />
</IconButton>
<div class="aspect-square h-full rounded-full border border-neutral-700">
<IconButton on:click={() => (paused = !paused)}>
<div slot="icon">
{#if waiting}
<Loader size={1} />
<Loader size={1.5} />
{:else}
<i class="fa-solid {paused ? 'fa-play' : 'fa-pause'}" />
{/if}
</button>
<button class="aspect-square h-8" on:click={() => $queue.next()}>
<i class="fa-solid fa-forward-step" />
</button>
<button on:click={() => (loop = !loop)} class="aspect-square h-8">
<i class="fa-solid fa-repeat {loop ? 'text-lazuli-primary' : 'text-white'}" />
</button>
</div>
<div class="flex items-center justify-items-center gap-2">
</IconButton>
</div>
<IconButton on:click={() => $queue.clear()}>
<i slot="icon" class="fa-solid fa-stop text-xl" />
</IconButton>
<IconButton on:click={() => $queue.next()}>
<i slot="icon" class="fa-solid fa-forward-step text-xl" />
</IconButton>
<div class="flex flex-grow items-center justify-items-center gap-3 font-light">
<span bind:this={currentTimeTimestamp} class="w-16 text-right" />
<div class="w-72">
<Slider
bind:this={progressBar}
max={duration}
@@ -139,16 +152,11 @@
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={() => (volume = volume > 0 ? 0 : Number(localStorage.getItem('volume')))} class="aspect-square h-8">
<i class="fa-solid {volume > maxVolume / 2 ? '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">
<section class="flex items-center justify-end gap-2.5 py-6 text-lg">
<div id="volume-slider" class="mx-4 flex h-10 w-44 flex-row-reverse items-center gap-3">
<Slider
bind:value={volume}
max={maxVolume}
@@ -156,24 +164,29 @@
if (volume > 0) localStorage.setItem('volume', volume.toString())
}}
/>
<IconButton on:click={() => (volume = volume > 0 ? 0 : Number(localStorage.getItem('volume')))}>
<i slot="icon" class="fa-solid {volume > maxVolume / 2 ? 'fa-volume-high' : volume > 0 ? 'fa-volume-low' : 'fa-volume-xmark'}" />
</IconButton>
</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>
<IconButton on:click={() => (shuffled ? $queue.reorder() : $queue.shuffle())}>
<i slot="icon" class="fa-solid fa-shuffle {shuffled ? 'text-lazuli-primary' : 'text-white'}" />
</IconButton>
<IconButton on:click={() => (loop = !loop)}>
<i slot="icon" class="fa-solid fa-repeat {loop ? 'text-lazuli-primary' : 'text-white'}" />
</IconButton>
<IconButton on:click={() => (expanded = true)}>
<i slot="icon" class="fa-solid fa-chevron-up" />
</IconButton>
</section>
</main>
{:else}
<main id="expanded-player" in:fade={{ delay: 500 }} out:fade={{ duration: 75 }} class="relative h-full" style="--currentlyPlayingImage: url(/api/remoteImage?url={currentlyPlaying.thumbnailUrl});">
<img class="absolute -z-10 h-full w-full object-cover object-center blur-xl brightness-[25%]" src="/api/remoteImage?url={currentlyPlaying.thumbnailUrl}" alt="" />
<main id="expanded-player" in:fade={{ delay: 500 }} out:fade={{ duration: 75 }} class="relative h-full">
<div class="absolute -z-10 h-full w-full blur-xl brightness-[25%]">
<LazyImage thumbnailUrl={currentlyPlaying.thumbnailUrl} alt={''} objectFit={'cover'} />
</div>
<section id="song-queue-wrapper" class="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.thumbnailUrl}" alt="" />
{/key}
<LazyImage thumbnailUrl={currentlyPlaying.thumbnailUrl} alt={`${currentlyPlaying.name} jacket`} objectFit={'contain'} objectPosition={'left'} />
</section>
<section class="no-scrollbar flex max-h-full flex-col gap-3 overflow-y-scroll">
<strong class="ml-2 text-2xl">UP NEXT</strong>
@@ -181,13 +194,15 @@
{@const isCurrent = item === currentlyPlaying}
<button
on:click={() => {
if (!isCurrent) $queue.current = item
if (!isCurrent) $queue.setCurrent(item)
}}
class="queue-item h-20 w-full shrink-0 items-center gap-3 overflow-clip rounded-lg bg-neutral-900 {isCurrent
? 'pointer-events-none border-[1px] border-neutral-300'
: 'hover:bg-neutral-800'}"
>
<div class="h-20 w-20 bg-cover bg-center" style="background-image: url('/api/remoteImage?url={item.thumbnailUrl}');" />
<div class="h-20 w-20">
<LazyImage thumbnailUrl={item.thumbnailUrl} alt={`${item.name} jacket`} objectFit={'cover'} />
</div>
<div class="justify-items-left text-left">
<div class="line-clamp-1">{item.name}</div>
<div class="mt-[.15rem] line-clamp-1 text-neutral-400">{item.artists?.map((artist) => artist.name).join(', ') || item.uploader?.name}</div>
@@ -198,7 +213,7 @@
</section>
</section>
<section class="px-8">
<div id="progress-bar-expanded" class="mb-8">
<div id="progress-bar-expanded" class="mb-7">
<span bind:this={expandedCurrentTimeTimestamp} class="text-right" />
<Slider
bind:this={expandedProgressBar}
@@ -215,46 +230,30 @@
<span bind:this={expandedDurationTimestamp} class="text-left" />
</div>
<div id="expanded-controls">
<div class="flex flex-col gap-2 overflow-hidden">
<div bind:clientWidth={slidingTextWrapperWidth} class="relative h-9 w-full">
<strong
bind:this={slidingText}
bind:clientWidth={slidingTextWidth}
on:animationend={() => (scrollDirection *= -1)}
id="scrollingText"
class="{scrollDistance > 0 ? (scrollDirection > 0 ? 'scrollLeft' : 'scrollRight') : ''} absolute whitespace-nowrap text-3xl">{currentlyPlaying.name}</strong
>
<div class="flex flex-col gap-1.5 overflow-hidden">
<div class="h-9">
<ScrollingText>
<strong slot="text" class="text-3xl">{currentlyPlaying.name}</strong>
</ScrollingText>
</div>
{#if (currentlyPlaying.artists && currentlyPlaying.artists.length > 0) || currentlyPlaying.uploader}
<div class="line-clamp-1 flex flex-nowrap items-center font-extralight">
<i class="fa-solid fa-user mr-3 text-sm" />
<ArtistList mediaItem={currentlyPlaying} />
</div>
<div class="line-clamp-1 flex flex-nowrap" style="font-size: 0;">
{#if currentlyPlaying.artists && currentlyPlaying.artists.length > 0}
{#each currentlyPlaying.artists as artist, index}
<a
on:click={() => (expanded = false)}
class="line-clamp-1 flex-shrink-0 text-lg hover:underline focus:underline"
href="/details/artist?id={artist.id}&connection={currentlyPlaying.connection.id}">{artist.name}</a
>
{#if index < currentlyPlaying.artists.length - 1}
<span class="mr-1 text-lg">,</span>
{/if}
{/each}
{:else if currentlyPlaying.uploader}
<a
on:click={() => (expanded = false)}
class="line-clamp-1 flex-shrink-0 text-lg hover:underline focus:underline"
href="/details/user?id={currentlyPlaying.uploader.id}&connection={currentlyPlaying.connection.id}">{currentlyPlaying.uploader.name}</a
>
{/if}
{#if currentlyPlaying.album}
<span class="mx-1.5 text-lg">-</span>
<div class="flex flex-nowrap items-center font-extralight">
<i class="fa-solid fa-compact-disc mr-3 text-sm" />
<a
on:click={() => (expanded = false)}
class="line-clamp-1 flex-shrink-0 text-lg hover:underline focus:underline"
class="line-clamp-1 flex-shrink-0 hover:underline focus:underline"
href="/details/album?id={currentlyPlaying.album.id}&connection={currentlyPlaying.connection.id}">{currentlyPlaying.album.name}</a
>
</div>
{/if}
</div>
</div>
<div class="flex w-full items-center justify-center gap-2 text-2xl">
<div class="flex h-min w-full items-center justify-center gap-2 text-2xl">
<button on:click={() => (shuffled ? $queue.reorder() : $queue.shuffle())} class="aspect-square h-16">
<i class="fa-solid {shuffled ? 'fa-shuffle' : 'fa-right-left'}" />
</button>
@@ -275,7 +274,7 @@
<i class="fa-solid fa-repeat {loop ? 'text-lazuli-primary' : 'text-white'}" />
</button>
</div>
<section class="flex items-center justify-end gap-2 text-xl">
<section class="flex h-min 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={() => (volume = volume > 0 ? 0 : Number(localStorage.getItem('volume')))} class="aspect-square h-8">
<i class="fa-solid {volume > maxVolume / 2 ? 'fa-volume-high' : volume > 0 ? 'fa-volume-low' : 'fa-volume-xmark'} w-full text-center" />
@@ -347,41 +346,4 @@
gap: 1rem;
grid-template-columns: 1fr min-content 1fr !important;
}
#scrollingText {
animation-timing-function: linear;
animation-fill-mode: both;
animation-delay: 10s;
}
#scrollingText:hover {
animation-play-state: paused;
}
.scrollLeft {
animation-name: scrollLeft;
}
.scrollRight {
animation-name: scrollRight;
}
@keyframes scrollLeft {
0% {
left: 0%;
transform: translateX(0%);
}
100% {
left: 100%;
transform: translateX(-100%);
}
}
@keyframes scrollRight {
0% {
left: 100%;
transform: translateX(-100%);
}
100% {
left: 0%;
transform: translateX(0%);
}
}
</style>

View File

@@ -0,0 +1,69 @@
<!--
@component
A component that can be injected with a text to display it in a single line that clips if overflowing and intermittently scrolls
from one end to the other. The scrolling text area is set to take up the maximum width and height that it can. Constrain the available
scrolling area with a wrapper element.
```tsx
<slot name="text" /> // An HTML element to wrap and style the text you want to scroll (e.g. div, spans, strongs)
```
-->
<script lang="ts">
let slidingText: HTMLElement
let slidingTextWidth: number, slidingTextWrapperWidth: number
let scrollDirection: 1 | -1 = 1
$: scrollDistance = slidingTextWidth - slidingTextWrapperWidth
$: if (slidingText && scrollDistance > 0) slidingText.style.animationDuration = `${scrollDistance / 40}s`
</script>
<div bind:clientWidth={slidingTextWrapperWidth} class="relative h-full w-full overflow-clip">
<span
bind:this={slidingText}
bind:clientWidth={slidingTextWidth}
on:animationend={() => (scrollDirection *= -1)}
id="scrollingText"
class="{scrollDistance > 0 ? (scrollDirection > 0 ? 'scrollLeft' : 'scrollRight') : ''} absolute whitespace-nowrap"
>
<slot name="text" />
</span>
</div>
<style>
#scrollingText {
animation-timing-function: linear;
animation-fill-mode: both;
animation-delay: 10s;
}
#scrollingText:hover {
animation-play-state: paused;
}
.scrollLeft {
animation-name: scrollLeft;
}
.scrollRight {
animation-name: scrollRight;
}
@keyframes scrollLeft {
0% {
left: 0%;
transform: translateX(0%);
}
100% {
left: 100%;
transform: translateX(-100%);
}
}
@keyframes scrollRight {
0% {
left: 100%;
transform: translateX(-100%);
}
100% {
left: 0%;
transform: translateX(0%);
}
}
</style>

View File

@@ -46,18 +46,6 @@ class Queue {
return this.currentSongs[this.currentPosition]
}
/** Sets the currently playing song to the song provided as long as it is in the current playlist */
set current(newSong: Song | null) {
if (newSong === null) {
this.currentPosition = -1
} else {
const queuePosition = this.currentSongs.findIndex((song) => song === newSong)
if (queuePosition >= 0) this.currentPosition = queuePosition
}
this.updateQueue()
}
get list() {
return this.currentSongs
}
@@ -66,6 +54,14 @@ class Queue {
return this.shuffled
}
/** Sets the currently playing song to the song provided as long as it is in the current playlist */
public setCurrent(newSong: Song) {
const queuePosition = this.currentSongs.findIndex((song) => song === newSong)
if (queuePosition < 0) return
this.currentPosition = queuePosition
this.updateQueue()
}
/** Shuffles all songs in the queue after the currently playing song */
public shuffle() {
const shuffledSongs = fisherYatesShuffle(this.currentSongs.slice(this.currentPosition + 1))

View File

@@ -2,7 +2,8 @@
import { goto } from '$app/navigation'
import { itemDisplayState } from '$lib/stores'
import type { LayoutData } from './$types.js'
import { fly, fade } from 'svelte/transition'
import { fade } from 'svelte/transition'
import IconButton from '$lib/components/util/iconButton.svelte'
export let data: LayoutData
@@ -17,13 +18,13 @@
<button disabled={/^\/library\/artists.*$/.test(currentPathname)} class="library-tab h-full px-1" on:click={() => goto('/library/artists')}>Artists</button>
<button disabled={/^\/library\/collection.*$/.test(currentPathname)} class="library-tab h-full px-1" on:click={() => goto('/library/collection')}>My Collection</button>
</section>
<section class="h-full justify-self-end">
<button disabled={$itemDisplayState === 'list'} class="view-toggle aspect-square h-full" on:click={() => ($itemDisplayState = 'list')}>
<i class="fa-solid fa-list" />
</button>
<button disabled={$itemDisplayState === 'grid'} class="view-toggle aspect-square h-full" on:click={() => ($itemDisplayState = 'grid')}>
<i class="fa-solid fa-grip" />
</button>
<section class="flex h-full justify-self-end">
<IconButton disabled={$itemDisplayState === 'list'} on:click={() => ($itemDisplayState = 'list')}>
<i slot="icon" class="fa-solid fa-list {$itemDisplayState === 'list' ? 'text-lazuli-primary' : 'text-white'}" />
</IconButton>
<IconButton disabled={$itemDisplayState === 'grid'} on:click={() => ($itemDisplayState = 'grid')}>
<i slot="icon" class="fa-solid fa-grip {$itemDisplayState === 'grid' ? 'text-lazuli-primary' : 'text-white'}" />
</IconButton>
</section>
</nav>
{#key currentPathname}
@@ -34,9 +35,6 @@
</main>
<style>
button.view-toggle[disabled] {
color: var(--lazuli-primary);
}
button.library-tab[disabled] {
color: var(--lazuli-primary);
border-top: 2px solid var(--lazuli-primary);

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import LazyImage from '$lib/components/media/lazyImage.svelte'
import IconButton from '$lib/components/util/iconButton.svelte'
import ArtistList from '$lib/components/media/artistList.svelte'
import { goto } from '$app/navigation'
import { queue, newestAlert } from '$lib/stores'
@@ -26,7 +27,7 @@
<div class="p-3">
<div id="thumbnail-wrapper" class="relative aspect-square w-full overflow-clip rounded-lg">
<button id="thumbnail" class="h-full w-full" on:click={() => goto(`/details/album?id=${album.id}&connection=${album.connection.id}`)}>
<LazyImage thumbnailUrl={album.thumbnailUrl} alt={`${album.name} jacket`} />
<LazyImage thumbnailUrl={album.thumbnailUrl} alt={`${album.name} jacket`} objectFit={'cover'} />
</button>
<div id="play-button" class="absolute left-1/2 top-1/2 h-1/4 -translate-x-1/2 -translate-y-1/2 opacity-0">
<IconButton halo={true} on:click={playAlbum}>
@@ -36,17 +37,8 @@
</div>
<div class="py-2 text-center text-sm">
<div class="line-clamp-2">{album.name}</div>
<div class="line-clamp-2 flex justify-center text-neutral-400">
{#if album.artists === 'Various Artists'}
<span>Various Artists</span>
{:else}
{#each album.artists as artist, index}
<a class="hover:underline focus:underline" href="/details/artist?id={artist.id}&connection={album.connection.id}">{artist.name}</a>
{#if index < album.artists.length - 1}
&#44&#160
{/if}
{/each}
{/if}
<div class="line-clamp-2 text-neutral-400">
<ArtistList mediaItem={album} />
</div>
</div>
</div>