feat: currently playing + volume + seek position + toggle pause
This commit is contained in:
@@ -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 />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user