feat: currently playing + volume + seek position + toggle pause

This commit is contained in:
2024-11-24 23:35:14 +01:00
parent 7ae47d02f2
commit 2b471c6296
19 changed files with 1015 additions and 77 deletions

View File

@@ -9,6 +9,9 @@
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={$page.url.pathname === '/'} class="transition-all">
{#snippet tooltipContent()}
Home
{/snippet}
{#snippet child({ props })}
<a href="/" {...props}>
<Home />
@@ -25,6 +28,9 @@
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={$page.url.pathname === '/songs'} class="transition-all">
{#snippet tooltipContent()}
Songs
{/snippet}
{#snippet child({ props })}
<a href="/songs" {...props}>
<Music2 />
@@ -35,6 +41,9 @@
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={$page.url.pathname === '/albums'} class="transition-all">
{#snippet tooltipContent()}
Albums
{/snippet}
{#snippet child({ props })}
<a href="/albums" {...props}>
<Library />
@@ -45,6 +54,9 @@
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={$page.url.pathname === '/playlists'} class="transition-all">
{#snippet tooltipContent()}
Playlists
{/snippet}
{#snippet child({ props })}
<a href="/playlists" {...props}>
<ListMusic />
@@ -60,6 +72,9 @@
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={$page.url.pathname === '/settings'} class="transition-all">
{#snippet tooltipContent()}
Settings
{/snippet}
{#snippet child({ props })}
<a href="/settings" {...props}>
<Settings />

View File

@@ -16,65 +16,117 @@
import { Slider } from '$lib/components/ui/slider';
import * as Popover from '$lib/components/ui/popover';
import { Separator } from '$lib/components/ui/separator';
import { currentlyPlaying, volume } from '$lib/stores/player';
import { getPlayerState } from '$lib/player.svelte';
import { enhance } from '$app/forms';
import { getCoverUrl } from '$lib/covers';
import type { SubmitFunction } from '../../../routes/player/$types';
dayjs.extend(duration);
let progressValue = $state(0);
const player = getPlayerState();
let isPlaying = $state(false);
let trackDuration = $derived(
player.currentlyPlaying ? Number(player.currentlyPlaying.duration) : 0
);
/* async function seekProgressValue(e: MouseEvent) {
if (!$currentlyPlaying) {
let mouse = $state({
offsetX: 0,
offsetY: 0
});
let progressBar = $state<HTMLElement | null>(null);
let volumeForm = $state<HTMLFormElement | null>(null);
const seekProgressValue: SubmitFunction = ({ formData, cancel }) => {
if (!player.currentlyPlaying || !progressBar) {
cancel();
return;
}
const target = e.currentTarget! as HTMLElement;
const _targetProgress = Math.max(0.0, Math.min(1.0, e.offsetX / target.offsetWidth));
} */
const targetProgress = Math.round(
Math.max(0.0, Math.min(1.0, mouse.offsetX / progressBar.offsetWidth)) *
Number(player.currentlyPlaying.duration)
);
formData.append('position', targetProgress.toString());
return async ({ update, result }) => {
await update({
invalidateAll: false
});
if (result.type === 'success' && result.data && 'position' in result.data) {
player.progress = result.data.position;
}
};
};
async function skipToQueueIndex(index: number) {
console.log(`SKIP TO QUEUE INDEX ${index}`);
}
let volumeValue = {
const submitTogglePause: SubmitFunction = async () => {
return async ({ update, result }) => {
await update({
invalidateAll: false
});
if (result.type === 'success' && result.data && 'isPaused' in result.data) {
player.isPaused = result.data.isPaused;
}
};
};
function onMouseMove(e: MouseEvent) {
mouse.offsetX = e.offsetX;
mouse.offsetY = e.offsetY;
}
let volume = $state({
get value() {
return [$volume];
return [player.volume];
},
set value(v: number[]) {
$volume = v[0];
// TODO: Send message to player
player.volume = v[0];
}
};
});
</script>
<footer class="border border-x-0 border-t border-border/40 bg-background/95 px-4">
<nav class="my-2 grid w-full grid-cols-5 items-center">
<div class="col-span-1 flex items-end gap-2">
{#if $currentlyPlaying}
{#if player.currentlyPlaying}
<img
class="w-16 rounded-md shadow-2xl shadow-primary/50"
src="https://i.scdn.co/image/ab67616d0000b2732c0ead8ce0dd1c6e2fca817f"
alt={$currentlyPlaying}
class="aspect-square w-16 rounded-md shadow-2xl shadow-primary/50"
src={getCoverUrl(player.currentlyPlaying.hash)}
alt={player.currentlyPlaying.name}
/>
<div class="space-y-1 text-sm">
<h3 class="font-medium leading-none">Song name</h3>
<p class="text-xs text-muted-foreground">Artist name</p>
<h3 class="font-medium leading-none">{player.currentlyPlaying.name}</h3>
<p class="text-xs text-muted-foreground">{player.currentlyPlaying.artistName}</p>
</div>
{:else}
<div class="h-16 w-16 rounded-md bg-muted shadow-2xl shadow-primary/50"></div>
{/if}
</div>
<form class="col-span-3 flex justify-center gap-1" method="POST" use:enhance>
<form
class="col-span-3 flex justify-center gap-1"
method="POST"
use:enhance={submitTogglePause}
>
<Button variant="outline" size="icon">
<SkipBack />
</Button>
<Button type="submit" formaction="/player?/pause" variant="outline" size="icon">
{#if isPlaying}
<Pause />
{:else}
<Button
type="submit"
formaction="/player?/{player.isPaused ? 'resume' : 'pause'}"
variant="outline"
size="icon"
>
{#if player.isPaused}
<Play />
{:else}
<Pause />
{/if}
</Button>
<Button variant="outline" size="icon">
@@ -129,32 +181,67 @@
<div class="flex grow flex-row items-center gap-4">
<div class="min-w-6 text-muted-foreground">
{#if $volume <= 0}
{#if player.volume <= 0.0}
<VolumeX class="size-full" />
{:else if $volume < 50.0}
{:else if player.volume < 0.5}
<Volume1 class="size-full" />
{:else}
<Volume2 class="size-full" />
{/if}
</div>
<div class="flex-1">
<Slider class="w-full" min={0} max={1} step={0.01} bind:value={volumeValue.value} />
</div>
<form
bind:this={volumeForm}
class="flex-1"
method="POST"
action="/player?/volume"
use:enhance
>
<input type="hidden" name="volume" value={volume.value[0]} />
<Slider
class="w-full"
min={0}
max={1}
step={0.01}
bind:value={volume.value}
onValueChange={() => {
volumeForm?.requestSubmit();
}}
onValueCommit={() => {
volumeForm?.requestSubmit();
}}
/>
</form>
<div class="min-w-10 self-center">
<p class="text-sm text-muted-foreground">
{Math.round($volume * 100.0)}%
{Math.round(player.volume * 100.0)}%
</p>
</div>
</div>
</div>
</nav>
<div class="mb-2 flex flex-row items-center gap-2">
<span class="w-12 text-left text-sm">{dayjs.duration(0, 'seconds').format('mm:ss')}</span>
<button class="w-full">
{#key progressValue}
<Progress class="pointer-events-none w-full" value={progressValue} max={100} />
{/key}
</button>
<span class="w-12 text-right text-sm">{dayjs.duration(0, 'seconds').format('mm:ss')}</span>
<span class="w-12 text-left text-sm"
>{dayjs.duration(Number(player.progress), 'milliseconds').format('mm:ss')}</span
>
<form
class="w-full"
method="POST"
action="/player?/seek"
use:enhance={seekProgressValue}
bind:this={progressBar}
>
<button class="w-full" type="submit">
<Progress
class="pointer-events-none h-3 w-full"
value={Number(player.progress)}
max={player.currentlyPlaying ? Number(player.currentlyPlaying.duration) : 1}
/>
</button>
</form>
<span class="w-12 text-right text-sm"
>{dayjs.duration(trackDuration, 'milliseconds').format('mm:ss')}</span
>
</div>
</footer>
<svelte:window onmousemove={onMouseMove} />

View File

@@ -1,9 +1,11 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { getCoverUrl } from '$lib/covers';
import { getPlayerState } from '$lib/player.svelte';
// import { AudioLines } from 'lucide-svelte';
import type { Song } from '$lib/song';
import type { SubmitFunction } from '../../../routes/songs/[hash]/$types';
interface Props {
song: Song;
@@ -11,15 +13,30 @@
let { song }: Props = $props();
async function playSong(e: MouseEvent) {
e.preventDefault();
// TODO: Play song
}
const player = getPlayerState();
const submitPlaySong: SubmitFunction = async () => {
return async ({ update, result }) => {
await update({
invalidateAll: false
});
if (result.type === 'success' && result.data) {
player.currentlyPlaying = result.data.track ?? null;
player.progress = result.data.position;
}
};
};
</script>
<form class="flex flex-col gap-3" method="POST" action="/songs/{song.hash}?/play" use:enhance>
<form
class="flex flex-col gap-3"
method="POST"
action="/songs/{song.hash}?/play"
use:enhance={submitPlaySong}
>
<div class="relative">
<button type="submit" class="relative overflow-hidden rounded-lg" oncontextmenu={playSong}>
<button type="submit" class="relative overflow-hidden rounded-lg">
<img
class="aspect-square w-auto transition-all duration-150 hover:scale-105 hover:saturate-150"
src={getCoverUrl(song.hash)}