Skip to content

Commit

Permalink
Add audio queue capabilities
Browse files Browse the repository at this point in the history
  • Loading branch information
Mystler committed Sep 21, 2024
1 parent 5e8a097 commit d81f07b
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 23 deletions.
15 changes: 14 additions & 1 deletion src/lib/audioplayer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { writable } from "svelte/store";
import { writable, type Writable } from "svelte/store";
import type AudioPlayer from "$lib/components/AudioPlayer.svelte";
import { browser } from "$app/environment";

Expand All @@ -9,3 +9,16 @@ export const GlobalAudioVolume = writable<number>((browser && localStorage?.audi
GlobalAudioVolume.subscribe((v) => {
if (browser) localStorage.audioVolume = v;
});

export interface PlaylistEntry {
id: number;
title: string;
url: string;
}

export const GlobalPlaylist = writable<PlaylistEntry[]>([]);

let plSeq = 0;
export function addSong(playlist: Writable<PlaylistEntry[]>, url: string, title: string) {
if (playlist) playlist.update((pl) => [...pl, { url, title, id: plSeq++ }]);
}
54 changes: 41 additions & 13 deletions src/lib/components/AudioCard.svelte
Original file line number Diff line number Diff line change
@@ -1,31 +1,59 @@
<script lang="ts">
import { GlobalAudioCurrentSong, GlobalAudioPlayer } from "$lib/audioplayer";
import {
addSong,
GlobalAudioCurrentSong,
GlobalAudioPlayer,
GlobalPlaylist,
type PlaylistEntry,
} from "$lib/audioplayer";
import { getContext } from "svelte";
import type { Writable } from "svelte/store";
export let src: string;
export let title: string;
export let genre: string | null = null;
// Register our song into our parents list for "play all" tracking
addSong(getContext<Writable<PlaylistEntry[]>>("songs"), src, title);
function play() {
$GlobalAudioPlayer?.playSong(src, title);
}
</script>

<button
type="button"
class="audio-card {$GlobalAudioCurrentSong === src ? 'current-song' : ''}"
on:click={play}
>
<i class="fa fa-play text-xl"></i>
<b>{title}</b>
{#if genre}
<span class="text-zinc-400 text-sm grow content-end">{genre}</span>
{/if}
</button>
<div class="relative">
<button
type="button"
class="audio-card {$GlobalAudioCurrentSong === src ? 'current-song' : ''}"
on:click={play}
>
<i class="fa fa-play text-xl"></i>
<b>{title}</b>
{#if genre}
<span class="text-zinc-400 text-sm grow content-end">{genre}</span>
{/if}
</button>
<button
type="button"
title="Add to Queue"
class="absolute -bottom-1 -right-1 size-8 rounded-full bg-sky-900 hover:text-white hover:bg-sky-700"
on:click={(e) => {
addSong(GlobalPlaylist, src, title);
const button = e.currentTarget;
button.classList.add("animate-ping");
button.disabled = true;
setTimeout(() => {
button.classList.remove("animate-ping");
button.disabled = false;
}, 800);
}}><i class="fa fa-plus text-lg"></i></button
>
</div>

<style lang="postcss">
.audio-card {
@apply p-4 rounded-xl bg-zinc-900 hover:bg-zinc-700 text-sky-400 hover:text-white text-center;
@apply flex flex-col items-center gap-2;
@apply flex flex-col items-center gap-2 w-full h-full;
}
.current-song {
Expand Down
26 changes: 26 additions & 0 deletions src/lib/components/AudioCardList.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
<script lang="ts">
import { addSong, GlobalPlaylist, type PlaylistEntry } from "$lib/audioplayer";
import { setContext } from "svelte";
import { writable } from "svelte/store";
// Allow Audio Card elements to register with this context
const listedSongs = setContext("songs", writable<PlaylistEntry[]>([]));
</script>

<button
type="button"
class="p-4 mb-4 rounded-xl bg-zinc-900 text-sky-400 hover:text-white hover:bg-zinc-700"
on:click={(e) => {
for (const song of $listedSongs) {
addSong(GlobalPlaylist, song.url, song.title);
}
const button = e.currentTarget;
button.classList.add("animate-ping");
button.disabled = true;
setTimeout(() => {
button.classList.remove("animate-ping");
button.disabled = false;
}, 800);
}}><i class="fa fa-plus text-lg"></i> Add all to queue</button
>

<div class="audio-card-list">
<slot />
</div>
Expand Down
71 changes: 63 additions & 8 deletions src/lib/components/AudioPlayer.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
<script lang="ts">
import { GlobalAudioVolume } from "$lib/audioplayer";
import { GlobalAudioVolume, type PlaylistEntry } from "$lib/audioplayer";
import { writable, type Writable } from "svelte/store";
import PlaylistViewer from "./PlaylistViewer.svelte";
export let playlist: Writable<PlaylistEntry[]> = writable([]);
let url: string | null = null;
let name: string;
let time: number;
let duration: number;
let paused: boolean = true;
let pendingPlay: boolean = false;
let muted: boolean;
let readyState: number;
let audio: HTMLAudioElement;
let playlistViewer: PlaylistViewer;
function format(time: number): string {
if (isNaN(time)) return "...";
Expand All @@ -35,24 +41,48 @@
paused = !paused;
}
export function playSong(src: string | null, title: string) {
export function playSong(src: string | null, title: string, playNow: boolean = true) {
if (src !== url) {
// Different song, so reset a bunch of vars and kick off new play using pendingPlay
url = src;
name = title;
time = 0;
readyState = 0;
paused = true;
pendingPlay = playNow;
}
}
function playNext(now: boolean) {
if ($playlist.length > 0) {
playSong($playlist[0].url, $playlist[0].title, now);
[, ...$playlist] = $playlist;
}
}
// Restart the actual player when variables change and data is ready
$: {
if (url && audio && readyState > audio.HAVE_CURRENT_DATA) {
audio.play();
// Open player when no song is set but we add to queue.
if (!url && $playlist.length > 0) {
playNext(false);
}
}
// Restart the actual player when variables changed and data is ready
$: {
if (pendingPlay && url && audio && readyState > audio.HAVE_CURRENT_DATA) {
paused = false;
pendingPlay = false;
}
}
// Provide hook for base layout
export let onUrlChanged: (url: string | null) => void;
$: onUrlChanged(url);
function onEnded() {
time = 0;
playNext(true);
}
</script>

{#if url}
Expand All @@ -69,9 +99,7 @@
bind:duration
bind:paused
bind:readyState
on:ended={() => {
time = 0;
}}
on:ended={onEnded}
>
</audio>

Expand All @@ -86,6 +114,18 @@
<i class="fa {paused ? 'fa-play' : 'fa-pause'} text-2xl"></i>
</button>

{#if $playlist.length > 0}
<!-- Next button -->
<button
type="button"
title="Next"
class="btn size-12 shrink-0"
on:click={() => playNext(true)}
>
<i class="fa fa-forward-step text-2xl"></i>
</button>
{/if}

<!-- Group of Name and Position indicator -->
<div class="grow flex flex-col gap-1">
<b>{name}</b>
Expand Down Expand Up @@ -129,17 +169,32 @@
<button type="button" title="Download" class="icon-button">
<a href={url} download><i class="fa fa-download"></i></a>
</button>
<!-- Playlist link -->
<button
type="button"
title="Playlist"
class="icon-button"
on:click={() => playlistViewer.show()}
>
<i class="fa fa-list"></i>
</button>
<!-- Close button -->
<button
type="button"
title="Close"
class="icon-button"
on:click={() => {
url = null;
playlist.set([]);
}}><i class="fa fa-xmark"></i></button
>
</div>
</div>
<PlaylistViewer
bind:this={playlistViewer}
{playlist}
onPlayNow={(song) => playSong(song.url, song.title)}
/>
{/if}

<style lang="postcss">
Expand Down
Loading

0 comments on commit d81f07b

Please sign in to comment.