Trying out some UI changes. Added resizing support to remoteImage and LazyImage component
This commit is contained in:
64
src/lib/components/media/lazyImage.svelte
Normal file
64
src/lib/components/media/lazyImage.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<!--
|
||||
@component
|
||||
A component to help render images in a smooth and efficient way. The url passed will be fetched via Lazuli's
|
||||
remoteImage API endpoint with size parameters that are dynamically calculated base off of the image's container's
|
||||
width and height. Images are lazily loaded unless 'eager' loading is specified.
|
||||
|
||||
@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.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let thumbnailUrl: string
|
||||
export let alt: string
|
||||
export let loadingMethod: 'lazy' | 'eager' = 'lazy'
|
||||
|
||||
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
|
||||
|
||||
const width = imageContainer.clientWidth
|
||||
const height = imageContainer.clientHeight
|
||||
|
||||
const newImage = new Image(width, height)
|
||||
imageContainer.appendChild(newImage)
|
||||
|
||||
newImage.loading = loadingMethod
|
||||
newImage.src = `/api/remoteImage?url=${newThumbnailURL}&`.concat(width > height ? `maxWidth=${width}` : `maxHeight=${height}`)
|
||||
newImage.alt = alt
|
||||
|
||||
newImage.style.width = '100%'
|
||||
newImage.style.height = '100%'
|
||||
newImage.style.objectFit = 'cover'
|
||||
newImage.style.opacity = '0'
|
||||
newImage.style.position = 'absolute'
|
||||
|
||||
newImage.onload = () => {
|
||||
removeOldImage()
|
||||
newImage.style.transition = 'opacity 500ms ease'
|
||||
newImage.style.opacity = '1'
|
||||
}
|
||||
|
||||
newImage.onerror = () => {
|
||||
removeOldImage()
|
||||
newImage.style.opacity = '1'
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => updateImage(thumbnailUrl))
|
||||
$: updateImage(thumbnailUrl)
|
||||
</script>
|
||||
|
||||
<div bind:this={imageContainer} class="relative h-full w-full"></div>
|
||||
53
src/lib/components/media/listItem.svelte
Normal file
53
src/lib/components/media/listItem.svelte
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import LazyImage from './lazyImage.svelte'
|
||||
|
||||
export let mediaItem: Song | Album | Artist | Playlist
|
||||
|
||||
const thumbnailUrl = 'thumbnailUrl' in mediaItem ? mediaItem.thumbnailUrl : mediaItem.profilePicture
|
||||
|
||||
const date = 'releaseDate' in mediaItem && mediaItem.releaseDate ? new Date(mediaItem.releaseDate).getFullYear().toString() : 'releaseYear' in mediaItem ? mediaItem.releaseYear : undefined
|
||||
</script>
|
||||
|
||||
<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`} />
|
||||
{: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" />
|
||||
</div>
|
||||
{/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}
|
||||
, 
|
||||
{/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>
|
||||
{/if}
|
||||
</span>
|
||||
<div class="justify-self-center text-neutral-400">{date ?? ''}</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#list-item {
|
||||
display: grid;
|
||||
column-gap: 1rem;
|
||||
align-items: center;
|
||||
grid-template-columns: 4rem 1fr 1fr 5rem;
|
||||
}
|
||||
#thumbnail-placeholder {
|
||||
background: radial-gradient(circle, rgba(0, 0, 0, 0) 25%, rgba(35, 40, 50, 1) 100%);
|
||||
}
|
||||
</style>
|
||||
@@ -15,7 +15,7 @@
|
||||
}).then((response) => response.json() as Promise<{ items: Song[] }>)
|
||||
|
||||
const items = itemsResponse.items
|
||||
queueRef.setQueue({ songs: items })
|
||||
queueRef.setQueue(items)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
on:click={() => {
|
||||
switch (mediaItem.type) {
|
||||
case 'song':
|
||||
queueRef.setQueue({ songs: [mediaItem] })
|
||||
queueRef.setQueue([mediaItem])
|
||||
break
|
||||
case 'album':
|
||||
case 'playlist':
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// import { FastAverageColor } from 'fast-average-color'
|
||||
import Slider from '$lib/components/util/slider.svelte'
|
||||
import Loader from '$lib/components/util/loader.svelte'
|
||||
import LazyImage from './lazyImage.svelte'
|
||||
|
||||
$: currentlyPlaying = $queue.current
|
||||
|
||||
@@ -43,9 +44,9 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const storedVolume = localStorage.getItem('volume')
|
||||
if (storedVolume) {
|
||||
volume = Number(storedVolume)
|
||||
const storedVolume = Number(localStorage.getItem('volume'))
|
||||
if (storedVolume >= 0 && storedVolume <= maxVolume) {
|
||||
volume = storedVolume
|
||||
} else {
|
||||
localStorage.setItem('volume', (maxVolume / 2).toString())
|
||||
volume = maxVolume / 2
|
||||
@@ -89,14 +90,12 @@
|
||||
</script>
|
||||
|
||||
{#if currentlyPlaying}
|
||||
<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">
|
||||
<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">
|
||||
<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.thumbnailUrl});" />
|
||||
{/key}
|
||||
<LazyImage thumbnailUrl={currentlyPlaying.thumbnailUrl} alt={`${currentlyPlaying.name} jacket`} loadingMethod={'eager'} />
|
||||
</div>
|
||||
<section class="flex flex-col justify-center gap-1">
|
||||
<div class="line-clamp-2 text-sm">{currentlyPlaying.name}</div>
|
||||
@@ -168,9 +167,9 @@
|
||||
</section>
|
||||
</main>
|
||||
{:else}
|
||||
<main in:fade={{ delay: 500 }} out:fade={{ duration: 75 }} class="expanded-player relative h-full" style="--currentlyPlayingImage: url(/api/remoteImage?url={currentlyPlaying.thumbnailUrl});">
|
||||
<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="" />
|
||||
<section class="song-queue-wrapper h-full px-24 py-20">
|
||||
<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="" />
|
||||
@@ -199,7 +198,7 @@
|
||||
</section>
|
||||
</section>
|
||||
<section class="px-8">
|
||||
<div class="progress-bar-expanded mb-8">
|
||||
<div id="progress-bar-expanded" class="mb-8">
|
||||
<span bind:this={expandedCurrentTimeTimestamp} class="text-right" />
|
||||
<Slider
|
||||
bind:this={expandedProgressBar}
|
||||
@@ -215,14 +214,15 @@
|
||||
/>
|
||||
<span bind:this={expandedDurationTimestamp} class="text-left" />
|
||||
</div>
|
||||
<div class="expanded-controls">
|
||||
<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)}
|
||||
class="{scrollDistance > 0 ? (scrollDirection > 0 ? 'scrollLeft' : 'scrollRight') : ''} scrollingText absolute whitespace-nowrap text-3xl">{currentlyPlaying.name}</strong
|
||||
id="scrollingText"
|
||||
class="{scrollDistance > 0 ? (scrollDirection > 0 ? 'scrollLeft' : 'scrollRight') : ''} absolute whitespace-nowrap text-3xl">{currentlyPlaying.name}</strong
|
||||
>
|
||||
</div>
|
||||
<div class="line-clamp-1 flex flex-nowrap" style="font-size: 0;">
|
||||
@@ -320,11 +320,14 @@
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.expanded-player {
|
||||
#player-wrapper {
|
||||
filter: drop-shadow(0px 20px 20px #000000);
|
||||
}
|
||||
#expanded-player {
|
||||
display: grid;
|
||||
grid-template-rows: calc(100% - 12rem) 12rem;
|
||||
}
|
||||
.song-queue-wrapper {
|
||||
#song-queue-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 2fr;
|
||||
gap: 4rem;
|
||||
@@ -333,24 +336,24 @@
|
||||
display: grid;
|
||||
grid-template-columns: 5rem auto min-content;
|
||||
}
|
||||
.progress-bar-expanded {
|
||||
#progress-bar-expanded {
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto min-content;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
.expanded-controls {
|
||||
#expanded-controls {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: 1fr min-content 1fr !important;
|
||||
}
|
||||
|
||||
.scrollingText {
|
||||
#scrollingText {
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: both;
|
||||
animation-delay: 10s;
|
||||
}
|
||||
.scrollingText:hover {
|
||||
#scrollingText:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
.scrollLeft {
|
||||
|
||||
@@ -1,39 +1,23 @@
|
||||
<script lang="ts" context="module">
|
||||
export interface NavTab {
|
||||
pathname: string
|
||||
name: string
|
||||
icon: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let disabled = false
|
||||
export let nav: NavTab
|
||||
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
let button: HTMLButtonElement
|
||||
export let icon: string
|
||||
export let label: string
|
||||
export let redirect: string
|
||||
|
||||
export let disabled: boolean
|
||||
</script>
|
||||
|
||||
<button bind:this={button} class="relative grid aspect-square w-full place-items-center transition-colors" {disabled} on:click={() => goto(nav.pathname)}>
|
||||
<span class="pointer-events-none flex flex-col gap-2 text-xs">
|
||||
<i class="{nav.icon} text-xl" />
|
||||
{nav.name}
|
||||
<button {disabled} class="block w-full py-2 text-left" on:click={() => goto(redirect)}>
|
||||
<span class:disabled class="py-1 pl-8 {disabled ? 'text-lazuli-primary' : 'text-neutral-300'}">
|
||||
<i class="{icon} mr-1.5 h-5 w-5" />
|
||||
{label}
|
||||
</span>
|
||||
<div class="absolute left-0 top-1/2 h-0 w-[0.2rem] -translate-x-2 -translate-y-1/2 rounded-lg bg-white transition-all" />
|
||||
</button>
|
||||
|
||||
<style>
|
||||
button:disabled > div {
|
||||
height: 80%;
|
||||
}
|
||||
button:not(:disabled) {
|
||||
color: rgb(163 163, 163);
|
||||
}
|
||||
button:not(:disabled):hover {
|
||||
color: var(--lazuli-primary);
|
||||
}
|
||||
button:not(:disabled):hover > div {
|
||||
height: 40%;
|
||||
span.disabled {
|
||||
border-left: 2px solid var(--lazuli-primary);
|
||||
background: linear-gradient(to right, var(--lazuli-primary) -25%, transparent 25%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
<script lang="ts" context="module">
|
||||
export interface PlaylistTab {
|
||||
id: string
|
||||
name: string
|
||||
thumbnail: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let disabled = false
|
||||
export let playlist: PlaylistTab
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let button: HTMLButtonElement
|
||||
|
||||
type ButtonCenter = {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
const calculateCenter = (button: HTMLButtonElement): ButtonCenter => {
|
||||
const rect = button.getBoundingClientRect()
|
||||
const x = (rect.left + rect.right) / 2
|
||||
const y = (rect.top + rect.bottom) / 2
|
||||
return { x, y }
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
{disabled}
|
||||
bind:this={button}
|
||||
class="relative aspect-square w-full flex-shrink-0 rounded-lg bg-cover bg-center transition-all"
|
||||
style="background-image: url({playlist.thumbnail});"
|
||||
on:mouseenter={() => dispatch('mouseenter', { ...calculateCenter(button), content: playlist.name })}
|
||||
on:mouseleave={() => dispatch('mouseleave')}
|
||||
on:click={() => {
|
||||
dispatch('click')
|
||||
goto(`/library?playlist=${playlist.id}`)
|
||||
}}
|
||||
>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
button:not(:disabled):not(:hover) {
|
||||
filter: brightness(50%);
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<search role="search" bind:this={searchBar} class="relative flex h-12 w-full items-center gap-2.5 justify-self-center rounded-full border-2 border-transparent px-4 py-2" style="background-color: rgba(82, 82, 82, 0.25);">
|
||||
<search
|
||||
role="search"
|
||||
bind:this={searchBar}
|
||||
class="relative flex h-10 w-full min-w-60 max-w-screen-sm items-center gap-2.5 rounded-lg border-2 border-transparent px-4 py-2"
|
||||
style="background-color: rgba(82, 82, 82, 0.25);"
|
||||
>
|
||||
<button
|
||||
class="aspect-square h-6 transition-colors duration-200 hover:text-lazuli-primary"
|
||||
on:click|preventDefault={() => {
|
||||
|
||||
@@ -25,8 +25,8 @@ export class Jellyfin implements Connection {
|
||||
}
|
||||
|
||||
public async getConnectionInfo() {
|
||||
const userEndpoint = `Users/${this.jellyfinUserId}`
|
||||
const systemEndpoint = 'System/Info'
|
||||
const userEndpoint = `/Users/${this.jellyfinUserId}`
|
||||
const systemEndpoint = '/System/Info'
|
||||
|
||||
const getUserData = () =>
|
||||
this.services
|
||||
@@ -42,8 +42,8 @@ export class Jellyfin implements Connection {
|
||||
|
||||
const [userData, systemData] = await Promise.all([getUserData(), getSystemData()])
|
||||
|
||||
if (!userData) console.error(`Fetch to ${userEndpoint.toString()} failed`)
|
||||
if (!systemData) console.error(`Fetch to ${systemEndpoint.toString()} failed`)
|
||||
if (!userData) console.error(`Fetch to ${userEndpoint} failed`)
|
||||
if (!systemData) console.error(`Fetch to ${systemEndpoint} failed`)
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
@@ -206,9 +206,11 @@ class JellyfinServices {
|
||||
}
|
||||
}
|
||||
|
||||
private getBestThumbnail = (item: JellyfinAPI.Song | JellyfinAPI.Album | JellyfinAPI.Artist | JellyfinAPI.Playlist) => {
|
||||
private getBestThumbnail(item: JellyfinAPI.Song | JellyfinAPI.Album | JellyfinAPI.Artist | JellyfinAPI.Playlist, placeholder: string): string
|
||||
private getBestThumbnail(item: JellyfinAPI.Song | JellyfinAPI.Album | JellyfinAPI.Artist | JellyfinAPI.Playlist, placeholder?: string): string | undefined
|
||||
private getBestThumbnail(item: JellyfinAPI.Song | JellyfinAPI.Album | JellyfinAPI.Artist | JellyfinAPI.Playlist, placeholder?: string): string | undefined {
|
||||
const imageItemId = item.ImageTags?.Primary ? item.Id : 'AlbumPrimaryImageTag' in item && item.AlbumPrimaryImageTag ? item.AlbumId : undefined
|
||||
return imageItemId ? this.serverUrl(`Items/${imageItemId}/Images/Primary`).toString() : jellyfinLogo
|
||||
return imageItemId ? this.serverUrl(`Items/${imageItemId}/Images/Primary`).toString() : placeholder
|
||||
}
|
||||
|
||||
public parseSong = (song: JellyfinAPI.Song): Song => ({
|
||||
@@ -216,8 +218,8 @@ class JellyfinServices {
|
||||
id: song.Id,
|
||||
name: song.Name,
|
||||
type: 'song',
|
||||
duration: Math.floor(song.RunTimeTicks / 10000000),
|
||||
thumbnailUrl: this.getBestThumbnail(song),
|
||||
duration: Math.round(song.RunTimeTicks / 10000000),
|
||||
thumbnailUrl: this.getBestThumbnail(song, jellyfinLogo),
|
||||
releaseDate: song.PremiereDate ? new Date(song.PremiereDate).toISOString() : undefined,
|
||||
artists: song.ArtistItems?.map((artist) => ({ id: artist.Id, name: artist.Name })),
|
||||
album: song.AlbumId && song.Album ? { id: song.AlbumId, name: song.Album } : undefined,
|
||||
@@ -229,7 +231,7 @@ class JellyfinServices {
|
||||
id: album.Id,
|
||||
name: album.Name,
|
||||
type: 'album',
|
||||
thumbnailUrl: this.getBestThumbnail(album),
|
||||
thumbnailUrl: this.getBestThumbnail(album, jellyfinLogo),
|
||||
artists: album.AlbumArtists?.map((artist) => ({ id: artist.Id, name: artist.Name })) ?? 'Various Artists',
|
||||
releaseYear: album.ProductionYear?.toString(),
|
||||
})
|
||||
@@ -247,7 +249,7 @@ class JellyfinServices {
|
||||
id: playlist.Id,
|
||||
name: playlist.Name,
|
||||
type: 'playlist',
|
||||
thumbnailUrl: this.getBestThumbnail(playlist),
|
||||
thumbnailUrl: this.getBestThumbnail(playlist, jellyfinLogo),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -268,6 +270,7 @@ class JellyfinLibraryManager {
|
||||
}
|
||||
|
||||
public async artists(): Promise<Artist[]> {
|
||||
// ? This returns just album artists instead of all artists like in finamp, but I might decide that I want to return all artists instead
|
||||
return this.services
|
||||
.request('/Artists/AlbumArtists?sortBy=SortName&sortOrder=Ascending&recursive=true')
|
||||
.then((response) => response.json() as Promise<{ Items: JellyfinAPI.Artist[] }>)
|
||||
|
||||
@@ -168,7 +168,7 @@ export class YouTubeMusic implements Connection {
|
||||
const playerResponse = await fetch('https://www.youtube.com/youtubei/v1/player', {
|
||||
headers: {
|
||||
// 'user-agent': 'com.google.android.youtube/17.36.4 (Linux; U; Android 12; GB) gzip', <-- I thought this was necessary but it appears it might not be?
|
||||
authorization: `Bearer ${await this.requestManager.accessToken}`, // * Including the access token is what enables access to premium content
|
||||
authorization: `Bearer ${await this.requestManager.accessToken}`, // * Including the access token is what enables access to premium content for some reason
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
@@ -935,7 +935,8 @@ function secondsFromISO8601(duration: string): number {
|
||||
* - https://yt3.googleusercontent.com
|
||||
* - https://yt3.ggpht.com
|
||||
* - https://music.youtube.com
|
||||
* - https://i.ytimg.com
|
||||
* - https://www.gstatic.com - Static images (e.g. a placeholder artist profile picture)
|
||||
* - https://i.ytimg.com - Video Thumbnails
|
||||
*
|
||||
* NOTE:
|
||||
* https://i.ytimg.com corresponds to videos, which follow the mqdefault...maxres resolutions scale. It is generally bad practice to use these as there is no way to scale them with query params, and there is no way to tell if a maxres.jpg exists or not.
|
||||
@@ -953,7 +954,7 @@ function extractLargestThumbnailUrl(thumbnails: Array<{ url: string; width: numb
|
||||
return bestThumbnailURL.slice(0, bestThumbnailURL.indexOf('='))
|
||||
case 'https://music.youtube.com':
|
||||
return bestThumbnailURL
|
||||
case 'https://www.gstatic.com': // This url will usually contain static images like a placeholder artist profile picture for example
|
||||
case 'https://www.gstatic.com':
|
||||
case 'https://i.ytimg.com':
|
||||
return bestThumbnailURL.slice(0, bestThumbnailURL.indexOf('?'))
|
||||
default:
|
||||
|
||||
@@ -8,6 +8,8 @@ export const newestAlert: Writable<[AlertType, string]> = writable()
|
||||
const youtubeMusicBackground: string = 'https://www.gstatic.com/youtube/media/ytm/images/sbg/wsbg@4000x2250.png' // Default Youtube music background
|
||||
export const backgroundImage: Writable<string> = writable(youtubeMusicBackground)
|
||||
|
||||
export const itemDisplayState: Writable<'list' | 'grid'> = writable('grid')
|
||||
|
||||
function fisherYatesShuffle<T>(items: T[]) {
|
||||
for (let currentIndex = items.length - 1; currentIndex >= 0; currentIndex--) {
|
||||
let randomIndex = Math.floor(Math.random() * (currentIndex + 1))
|
||||
@@ -17,10 +19,6 @@ function fisherYatesShuffle<T>(items: T[]) {
|
||||
return items
|
||||
}
|
||||
|
||||
// ? New idea for how to handle mixing. Keep originalSongs and currentSongs but also add playedSongs. Add the previous song to played songs whenever next() is called.
|
||||
// ? Whenever a song is mixed, set currentSongs = [...playedSongs, currentSongs[currentPosition], ...fisherYatesShuffle(everything else)]. Reorder method would stay the same.
|
||||
// ? IDK it's a thought
|
||||
|
||||
class Queue {
|
||||
private currentPosition: number // -1 means no song is playing
|
||||
private originalSongs: Song[]
|
||||
@@ -36,6 +34,11 @@ class Queue {
|
||||
this.shuffled = false
|
||||
}
|
||||
|
||||
private updateQueue() {
|
||||
writableQueue.set(this)
|
||||
// TODO: Implement Queue Saver
|
||||
}
|
||||
|
||||
get current() {
|
||||
if (this.currentSongs.length === 0) return null
|
||||
|
||||
@@ -52,7 +55,7 @@ class Queue {
|
||||
if (queuePosition >= 0) this.currentPosition = queuePosition
|
||||
}
|
||||
|
||||
writableQueue.set(this)
|
||||
this.updateQueue()
|
||||
}
|
||||
|
||||
get list() {
|
||||
@@ -68,7 +71,7 @@ class Queue {
|
||||
const shuffledSongs = fisherYatesShuffle(this.currentSongs.slice(this.currentPosition + 1))
|
||||
this.currentSongs = this.currentSongs.slice(0, this.currentPosition + 1).concat(shuffledSongs)
|
||||
this.shuffled = true
|
||||
writableQueue.set(this)
|
||||
this.updateQueue()
|
||||
}
|
||||
|
||||
/** Restores the queue to its original ordered state, while maintaining whatever song is currently playing */
|
||||
@@ -77,7 +80,7 @@ class Queue {
|
||||
this.currentSongs = [...this.originalSongs]
|
||||
this.currentPosition = originalPosition
|
||||
this.shuffled = false
|
||||
writableQueue.set(this)
|
||||
this.updateQueue()
|
||||
}
|
||||
|
||||
/** Starts the next song */
|
||||
@@ -85,7 +88,7 @@ class Queue {
|
||||
if (this.currentSongs.length === 0 || this.currentSongs.length <= this.currentPosition + 1) return
|
||||
|
||||
this.currentPosition += 1
|
||||
writableQueue.set(this)
|
||||
this.updateQueue()
|
||||
}
|
||||
|
||||
/** Plays the previous song */
|
||||
@@ -93,27 +96,27 @@ class Queue {
|
||||
if (this.currentSongs.length === 0 || this.currentPosition <= 0) return
|
||||
|
||||
this.currentPosition -= 1
|
||||
writableQueue.set(this)
|
||||
this.updateQueue()
|
||||
}
|
||||
|
||||
/** Add songs to the end of the queue */
|
||||
public enqueue(...songs: Song[]) {
|
||||
this.originalSongs.push(...songs)
|
||||
this.currentSongs.push(...songs)
|
||||
writableQueue.set(this)
|
||||
this.updateQueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param songs An ordered array of Songs
|
||||
* @param shuffled Whether or not to shuffle the queue before starting playback. False if not specified
|
||||
*/
|
||||
public setQueue(params: { songs: Song[]; shuffled?: boolean }) {
|
||||
if (params.songs.length === 0) return // Should not set a queue with no songs, use clear()
|
||||
this.originalSongs = params.songs
|
||||
this.currentSongs = params.shuffled ? fisherYatesShuffle(params.songs) : params.songs
|
||||
public setQueue(songs: Song[], shuffled?: boolean) {
|
||||
if (songs.length === 0) return // Should not set a queue with no songs, use clear()
|
||||
this.originalSongs = songs
|
||||
this.currentSongs = shuffled ? fisherYatesShuffle(songs) : songs
|
||||
this.currentPosition = 0
|
||||
this.shuffled = params.shuffled ?? false
|
||||
writableQueue.set(this)
|
||||
this.shuffled = shuffled ?? false
|
||||
this.updateQueue()
|
||||
}
|
||||
|
||||
/** Clears all items from the queue */
|
||||
@@ -121,7 +124,7 @@ class Queue {
|
||||
this.currentPosition = -1
|
||||
this.originalSongs = []
|
||||
this.currentSongs = []
|
||||
writableQueue.set(this)
|
||||
this.updateQueue()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user