- {dayjs(totalDuration, 'milliseconds').format('mm:ss')}
+ {dayjs.duration(totalDuration, 'milliseconds').format('mm:ss')}
{tracks.length} track{tracks.length !== 1 ? 's' : ''}
diff --git a/src/lib/components/groove/TrackListing.svelte b/src/lib/components/groove/TrackListing.svelte
index 8caceb8..a8f55d3 100644
--- a/src/lib/components/groove/TrackListing.svelte
+++ b/src/lib/components/groove/TrackListing.svelte
@@ -5,11 +5,10 @@
interface Props {
track: Track;
- currentlyPlaying?: boolean;
oncontextmenu?: (event: MouseEvent, hash: string) => any;
}
- let { track, currentlyPlaying = false, oncontextmenu }: Props = $props();
+ let { track, oncontextmenu }: Props = $props();
diff --git a/src/lib/components/ui/table/index.ts b/src/lib/components/ui/table/index.ts
new file mode 100644
index 0000000..14695c8
--- /dev/null
+++ b/src/lib/components/ui/table/index.ts
@@ -0,0 +1,28 @@
+import Root from "./table.svelte";
+import Body from "./table-body.svelte";
+import Caption from "./table-caption.svelte";
+import Cell from "./table-cell.svelte";
+import Footer from "./table-footer.svelte";
+import Head from "./table-head.svelte";
+import Header from "./table-header.svelte";
+import Row from "./table-row.svelte";
+
+export {
+ Root,
+ Body,
+ Caption,
+ Cell,
+ Footer,
+ Head,
+ Header,
+ Row,
+ //
+ Root as Table,
+ Body as TableBody,
+ Caption as TableCaption,
+ Cell as TableCell,
+ Footer as TableFooter,
+ Head as TableHead,
+ Header as TableHeader,
+ Row as TableRow,
+};
diff --git a/src/lib/components/ui/table/table-body.svelte b/src/lib/components/ui/table/table-body.svelte
new file mode 100644
index 0000000..6c20c01
--- /dev/null
+++ b/src/lib/components/ui/table/table-body.svelte
@@ -0,0 +1,16 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/table/table-caption.svelte b/src/lib/components/ui/table/table-caption.svelte
new file mode 100644
index 0000000..2b0cba5
--- /dev/null
+++ b/src/lib/components/ui/table/table-caption.svelte
@@ -0,0 +1,16 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/table/table-cell.svelte b/src/lib/components/ui/table/table-cell.svelte
new file mode 100644
index 0000000..d859856
--- /dev/null
+++ b/src/lib/components/ui/table/table-cell.svelte
@@ -0,0 +1,23 @@
+
+
+
[role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...restProps}
+>
+ {@render children?.()}
+ |
diff --git a/src/lib/components/ui/table/table-footer.svelte b/src/lib/components/ui/table/table-footer.svelte
new file mode 100644
index 0000000..0267c47
--- /dev/null
+++ b/src/lib/components/ui/table/table-footer.svelte
@@ -0,0 +1,16 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/table/table-head.svelte b/src/lib/components/ui/table/table-head.svelte
new file mode 100644
index 0000000..45b2896
--- /dev/null
+++ b/src/lib/components/ui/table/table-head.svelte
@@ -0,0 +1,23 @@
+
+
+
[role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...restProps}
+>
+ {@render children?.()}
+ |
diff --git a/src/lib/components/ui/table/table-header.svelte b/src/lib/components/ui/table/table-header.svelte
new file mode 100644
index 0000000..684a57b
--- /dev/null
+++ b/src/lib/components/ui/table/table-header.svelte
@@ -0,0 +1,16 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/table/table-row.svelte b/src/lib/components/ui/table/table-row.svelte
new file mode 100644
index 0000000..9e693bc
--- /dev/null
+++ b/src/lib/components/ui/table/table-row.svelte
@@ -0,0 +1,23 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/table/table.svelte b/src/lib/components/ui/table/table.svelte
new file mode 100644
index 0000000..e3a95b6
--- /dev/null
+++ b/src/lib/components/ui/table/table.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+ {@render children?.()}
+
+
diff --git a/src/lib/player.svelte.ts b/src/lib/player.svelte.ts
index 9f9013c..96570ba 100644
--- a/src/lib/player.svelte.ts
+++ b/src/lib/player.svelte.ts
@@ -3,6 +3,7 @@ import type { PlayerStatus } from './proto/player';
import { getContext, setContext } from 'svelte';
import { PlayerClient } from './proto/player.client';
import { protoTransport } from '../hooks.client';
+import { deserialize } from '$app/forms';
class PlayerState {
volume = $state(0.0);
@@ -27,6 +28,50 @@ class PlayerState {
stream.responses.onMessage((status) => this.applyStatus(status));
}
+ async playTrack(hash: string, fetch: typeof globalThis.fetch) {
+ const response = await fetch(`/tracks/${hash}?/play`, {
+ method: 'POST',
+ headers: {
+ 'x-sveltekit-action': 'true'
+ },
+ body: new FormData()
+ });
+
+ if (!response.ok) {
+ return;
+ }
+
+ const result = deserialize(await response.text());
+
+ if (result.type === 'success' && result.data) {
+ const track = result.data.track as Track | null;
+ const position = result.data.position as bigint;
+
+ this.currentlyPlaying = track;
+ this.progress = position;
+ }
+ }
+
+ async playPlaylist(id: number, fetch: typeof globalThis.fetch) {
+ const response = await fetch(`/playlists/${id}?/play`, {
+ method: 'POST',
+ headers: {
+ 'x-sveltekit-action': 'true'
+ },
+ body: new FormData()
+ });
+
+ if (!response.ok) {
+ return;
+ }
+
+ const result = deserialize(await response.text());
+
+ if (result.type === 'success' && result.data) {
+ this.applyStatus(result.data as unknown as PlayerStatus);
+ }
+ }
+
applyStatus(status: PlayerStatus) {
if (!this.adjustingVolume) {
this.volume = status.volume;
diff --git a/src/routes/playlists/[id]/+page.svelte b/src/routes/playlists/[id]/+page.svelte
new file mode 100644
index 0000000..13663af
--- /dev/null
+++ b/src/routes/playlists/[id]/+page.svelte
@@ -0,0 +1,78 @@
+
+
+{#if playlist}
+
+
+
+
+
{playlist.name}
+
+ {playlist.tracks.length} track{playlist.tracks.length !== 1 ? 's' : ''}
+
+
+ {dayjs.duration(totalDuration, 'milliseconds').format('HH:mm:ss')}
+
+
+
+
+
+
+
+
+
+
+ Position
+ Track
+
+ Artist
+ Duration
+
+
+
+
+ {#each playlist.tracks as track, i}
+ player.playTrack(track.hash, fetch)}>
+ {i + 1}
+
+
+
+ {track.name}
+ {track.artistName}
+ {dayjs
+ .duration(Number(track.duration), 'milliseconds')
+ .format('mm:ss')}
+
+ {/each}
+
+
+
+
+{:else}
+ 404
+{/if}