feat!: playlists
This commit is contained in:
115
src/lib/components/groove/PlaylistListing.svelte
Normal file
115
src/lib/components/groove/PlaylistListing.svelte
Normal file
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import type { Track } from '$lib/proto/library';
|
||||
import dayjs from 'dayjs';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import Trash2 from 'virtual:icons/lucide/trash-2';
|
||||
import { enhance } from '$app/forms';
|
||||
import Play from 'virtual:icons/lucide/play';
|
||||
import type { SubmitFunction } from '../../../routes/playlists/[id]/$types';
|
||||
import { getCoverUrl } from '$lib/covers';
|
||||
import { getLibraryState } from '$lib/library.svelte';
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
name: string;
|
||||
tracks: Track[];
|
||||
}
|
||||
|
||||
let { id, name, tracks }: Props = $props();
|
||||
let totalDuration = $derived<number>(
|
||||
tracks.reduce((acc, track) => acc + Number(track.duration), 0)
|
||||
);
|
||||
|
||||
const library = getLibraryState();
|
||||
|
||||
let coverImages = $derived.by(() => {
|
||||
if (tracks.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (tracks.length >= 4) {
|
||||
return tracks.slice(0, 4).map((t) => getCoverUrl(t.hash));
|
||||
}
|
||||
|
||||
if (tracks.length === 1) {
|
||||
return [getCoverUrl(tracks[0].hash)];
|
||||
}
|
||||
|
||||
if (tracks.length === 2) {
|
||||
const x = getCoverUrl(tracks[0].hash);
|
||||
const y = getCoverUrl(tracks[1].hash);
|
||||
return [x, y, y, x];
|
||||
}
|
||||
|
||||
let _covers: string[] = [];
|
||||
|
||||
let i = 0;
|
||||
while (_covers.length < 4) {
|
||||
_covers.push(getCoverUrl(tracks[i].hash));
|
||||
i = (i + 1) % tracks.length;
|
||||
}
|
||||
|
||||
return _covers;
|
||||
});
|
||||
|
||||
const playPlaylist: SubmitFunction = async () => {
|
||||
return async ({ update }) => {
|
||||
await update({
|
||||
invalidateAll: false,
|
||||
reset: false
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: Refactor
|
||||
const deletePlaylist: SubmitFunction = async ({ action }) => {
|
||||
const playlistId = parseInt(action.pathname.split('/playlists/')[1].split('/')[0]);
|
||||
|
||||
return async ({ update, result }) => {
|
||||
await update({
|
||||
invalidateAll: false,
|
||||
reset: false
|
||||
});
|
||||
|
||||
if (result.type === 'success') {
|
||||
library.playlists = library.playlists.filter((p) => p.id !== playlistId);
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="overflow-hidden rounded-md border">
|
||||
<div class="relative grid" class:grid-cols-2={coverImages.length > 1}>
|
||||
{#each coverImages as cover}
|
||||
<img class="aspect-square" src={cover} alt="" />
|
||||
{:else}
|
||||
<div class="w-full aspect-square animate-pulse bg-secondary"></div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="space-y-2 p-4">
|
||||
<div class="relative space-y-2">
|
||||
<h3 class="font-medium leading-none">{name}</h3>
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{dayjs(totalDuration, 'milliseconds').format('mm:ss')}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{tracks.length} track{tracks.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-between gap-1">
|
||||
<form method="POST" action="/playlists/{id}?/play" use:enhance={playPlaylist}>
|
||||
<Button type="submit" variant="outline" size="icon">
|
||||
<Play />
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="/playlists/{id}?/delete" use:enhance={deletePlaylist}>
|
||||
<Button type="submit" variant="outline" size="icon">
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user