125 lines
4.0 KiB
Svelte
125 lines
4.0 KiB
Svelte
<script lang="ts">
|
|
import { getLibraryState } from '$lib/library.svelte';
|
|
import { page } from '$app/stores';
|
|
import PlaylistCover from '$lib/components/groove/PlaylistCover.svelte';
|
|
import dayjs from 'dayjs';
|
|
import * as Table from '$lib/components/ui/table';
|
|
import duration from 'dayjs/plugin/duration';
|
|
import { getCoverUrl } from '$lib/covers';
|
|
import Play from 'virtual:icons/lucide/play';
|
|
import Trash2 from 'virtual:icons/lucide/trash-2';
|
|
import { getPlayerState } from '$lib/player.svelte';
|
|
import { Button } from '$lib/components/ui/button';
|
|
import * as ContextMenu from '$lib/components/ui/context-menu';
|
|
dayjs.extend(duration);
|
|
|
|
const library = getLibraryState();
|
|
const player = getPlayerState();
|
|
|
|
let playlist = $derived(library.playlists.get(Number($page.params.id)));
|
|
|
|
let totalDuration = $derived<number>(
|
|
playlist?.tracks.reduce((acc, track) => acc + Number(track.duration), 0) ?? 0
|
|
);
|
|
|
|
function onDragStart(e: DragEvent) {
|
|
const element = e.target as HTMLElement;
|
|
|
|
e.dataTransfer?.setData('text/plain', element.getAttribute('data-playlist-index')!);
|
|
}
|
|
|
|
async function onDrop(e: DragEvent) {
|
|
if (!playlist) {
|
|
return;
|
|
}
|
|
|
|
const aIndex = e.dataTransfer?.getData('text/plain')!;
|
|
const bIndex = (e.currentTarget as HTMLElement).getAttribute('data-playlist-index')!;
|
|
|
|
library.swapPlaylistTracks(playlist.id, Number(aIndex), Number(bIndex), fetch);
|
|
}
|
|
</script>
|
|
|
|
{#if playlist}
|
|
<div class="flex w-full flex-col gap-8">
|
|
<div class="flex flex-row items-center justify-center gap-4">
|
|
<div class="relative w-56 overflow-hidden rounded-lg shadow-2xl shadow-primary/50">
|
|
<PlaylistCover tracks={playlist.tracks} />
|
|
</div>
|
|
<div class="flex flex-col">
|
|
<h4 class="text-xl font-semibold tracking-tight">{playlist.name}</h4>
|
|
<p class="text-sm text-muted-foreground">
|
|
{playlist.tracks.length} track{playlist.tracks.length !== 1 ? 's' : ''}
|
|
</p>
|
|
<p class="text-sm text-muted-foreground">
|
|
{dayjs.duration(totalDuration, 'milliseconds').format('HH:mm:ss')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="mx-4">
|
|
<div class="mb-2">
|
|
<Button class="size-12" onclick={() => player.playPlaylist(playlist.id, fetch)}
|
|
><Play /></Button
|
|
>
|
|
</div>
|
|
<Table.Root>
|
|
<Table.Header>
|
|
<Table.Row>
|
|
<Table.Head class="w-0">Position</Table.Head>
|
|
<Table.Head class="w-0">Track</Table.Head>
|
|
<Table.Head class="w-0"></Table.Head>
|
|
<Table.Head class="w-0">Artist</Table.Head>
|
|
<Table.Head class="text-right">Duration</Table.Head>
|
|
</Table.Row>
|
|
</Table.Header>
|
|
<Table.Body>
|
|
{#each playlist.tracks as track, i}
|
|
<ContextMenu.Root>
|
|
<ContextMenu.Trigger>
|
|
{#snippet child({ props })}
|
|
<Table.Row
|
|
{...props}
|
|
class="select-none"
|
|
ondblclick={() => player.playTrack(track.hash, fetch)}
|
|
data-playlist-index={i}
|
|
ondragstart={onDragStart}
|
|
ondrop={onDrop}
|
|
ondragover={(e) => e.preventDefault()}
|
|
draggable
|
|
>
|
|
<Table.Cell class="text-center">{i + 1}</Table.Cell>
|
|
<Table.Cell class="w-0">
|
|
<img
|
|
class="overflow-hidden rounded-md"
|
|
src={getCoverUrl(track.hash)}
|
|
alt=""
|
|
/>
|
|
</Table.Cell>
|
|
<Table.Cell class="text-nowrap">{track.name}</Table.Cell>
|
|
<Table.Cell class="w-fit text-nowrap">{track.artistName}</Table.Cell>
|
|
<Table.Cell class="text-right"
|
|
>{dayjs
|
|
.duration(Number(track.duration), 'milliseconds')
|
|
.format('mm:ss')}</Table.Cell
|
|
>
|
|
</Table.Row>
|
|
{/snippet}
|
|
</ContextMenu.Trigger>
|
|
<ContextMenu.Content>
|
|
<ContextMenu.Item
|
|
onclick={() => library.removeTrackFromPlaylist(playlist.id, i, fetch)}
|
|
>
|
|
<Trash2 class="mr-1 size-4" />
|
|
Remove from Playlist
|
|
</ContextMenu.Item>
|
|
</ContextMenu.Content>
|
|
</ContextMenu.Root>
|
|
{/each}
|
|
</Table.Body>
|
|
</Table.Root>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
404
|
|
{/if}
|