feat!: playlists
This commit is contained in:
@@ -17,7 +17,7 @@
|
|||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"bits-ui": "^1.0.0-next.64",
|
"bits-ui": "^1.0.0-next.65",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"globals": "^15.0.0",
|
"globals": "^15.0.0",
|
||||||
"lucide-svelte": "^0.462.0",
|
"lucide-svelte": "^0.462.0",
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ package library;
|
|||||||
|
|
||||||
service Library {
|
service Library {
|
||||||
rpc ListTracks(google.protobuf.Empty) returns (TrackList);
|
rpc ListTracks(google.protobuf.Empty) returns (TrackList);
|
||||||
|
rpc ListPlaylists(google.protobuf.Empty) returns (ListPlaylistsResponse);
|
||||||
|
rpc CreatePlaylist(CreatePlaylistRequest) returns (CreatePlaylistResponse);
|
||||||
|
rpc DeletePlaylist(DeletePlaylistRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc AddTrackToPlaylist(AddTrackToPlaylistRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc RemoveTrackFromPlaylist(RemoveTrackFromPlaylistRequest) returns (google.protobuf.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
message TrackList {
|
message TrackList {
|
||||||
@@ -19,3 +24,35 @@ message Track {
|
|||||||
uint64 artist_id = 4;
|
uint64 artist_id = 4;
|
||||||
uint64 duration = 5;
|
uint64 duration = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Playlist {
|
||||||
|
uint32 id = 1;
|
||||||
|
string name = 2;
|
||||||
|
repeated Track tracks = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListPlaylistsResponse {
|
||||||
|
repeated Playlist playlists = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreatePlaylistRequest {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreatePlaylistResponse {
|
||||||
|
Playlist playlist = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeletePlaylistRequest {
|
||||||
|
uint32 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddTrackToPlaylistRequest {
|
||||||
|
uint32 playlist_id = 1;
|
||||||
|
string track_hash = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveTrackFromPlaylistRequest {
|
||||||
|
uint32 playlist_id = 1;
|
||||||
|
uint32 track_rank = 2;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ service Player {
|
|||||||
rpc SetVolume(SetVolumeRequest) returns (SetVolumeResponse);
|
rpc SetVolume(SetVolumeRequest) returns (SetVolumeResponse);
|
||||||
rpc PlayTrackNext(TrackRequest) returns (Queue);
|
rpc PlayTrackNext(TrackRequest) returns (Queue);
|
||||||
rpc AddTrackToQueue(TrackRequest) returns (Queue);
|
rpc AddTrackToQueue(TrackRequest) returns (Queue);
|
||||||
|
rpc AddTracksToQueue(TracksRequest) returns (Queue);
|
||||||
|
rpc PlayPlaylist(PlayPlaylistRequest) returns (PlayerStatus);
|
||||||
rpc SwapQueueIndices(SwapQueueIndicesRequest) returns (Queue);
|
rpc SwapQueueIndices(SwapQueueIndicesRequest) returns (Queue);
|
||||||
rpc SkipTrack(google.protobuf.Empty) returns (PlayerStatus);
|
rpc SkipTrack(google.protobuf.Empty) returns (PlayerStatus);
|
||||||
rpc SkipToQueueIndex(SkipToQueueIndexRequest) returns (PlayerStatus);
|
rpc SkipToQueueIndex(SkipToQueueIndexRequest) returns (PlayerStatus);
|
||||||
@@ -69,3 +71,11 @@ message SwapQueueIndicesRequest {
|
|||||||
uint32 a = 1;
|
uint32 a = 1;
|
||||||
uint32 b = 2;
|
uint32 b = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message TracksRequest {
|
||||||
|
repeated string tracks = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PlayPlaylistRequest {
|
||||||
|
uint32 id = 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import * as Sidebar from '$lib/components/ui/sidebar';
|
import * as Sidebar from '$lib/components/ui/sidebar';
|
||||||
import { Home, Library, ListMusic, Music2, Settings } from 'lucide-svelte';
|
import Home from 'virtual:icons/lucide/home';
|
||||||
|
import Library from 'virtual:icons/lucide/library';
|
||||||
|
import ListMusic from 'virtual:icons/lucide/list-music';
|
||||||
|
import Music2 from 'virtual:icons/lucide/music-2';
|
||||||
|
import Settings from 'virtual:icons/lucide/settings';
|
||||||
|
import CirclePlus from 'virtual:icons/lucide/circle-plus';
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
import CreatePlaylistDialog from './CreatePlaylistDialog.svelte';
|
||||||
|
import { getLibraryState } from '$lib/library.svelte';
|
||||||
|
|
||||||
|
const library = getLibraryState();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Sidebar.Root collapsible="icon">
|
<Sidebar.Root collapsible="icon">
|
||||||
@@ -64,6 +74,25 @@
|
|||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Sidebar.MenuButton>
|
</Sidebar.MenuButton>
|
||||||
|
<Sidebar.MenuSub>
|
||||||
|
<Sidebar.MenuSubItem>
|
||||||
|
<Sidebar.MenuSubButton class="cursor-pointer">
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<CreatePlaylistDialog class={cn(props.class ?? '', 'w-full')}>
|
||||||
|
<CirclePlus class="!text-inherit" />
|
||||||
|
<span>Create Playlist</span>
|
||||||
|
</CreatePlaylistDialog>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuSubButton>
|
||||||
|
</Sidebar.MenuSubItem>
|
||||||
|
{#each library.playlists as playlist}
|
||||||
|
<Sidebar.MenuSubItem>
|
||||||
|
<Sidebar.MenuSubButton class="cursor-pointer">
|
||||||
|
{playlist.name}
|
||||||
|
</Sidebar.MenuSubButton>
|
||||||
|
</Sidebar.MenuSubItem>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.MenuSub>
|
||||||
</Sidebar.MenuItem>
|
</Sidebar.MenuItem>
|
||||||
</Sidebar.Menu>
|
</Sidebar.Menu>
|
||||||
</Sidebar.Group>
|
</Sidebar.Group>
|
||||||
|
|||||||
69
src/lib/components/groove/CreatePlaylistDialog.svelte
Normal file
69
src/lib/components/groove/CreatePlaylistDialog.svelte
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import CirclePlus from 'virtual:icons/lucide/circle-plus';
|
||||||
|
import type { SubmitFunction } from '../../../routes/playlists/$types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children?: Snippet;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { children, ...props }: Props = $props();
|
||||||
|
|
||||||
|
let name = $state('');
|
||||||
|
let submitting = $state(false);
|
||||||
|
let isOpen = $state(false);
|
||||||
|
|
||||||
|
const createPlaylist: SubmitFunction = async ({ cancel }) => {
|
||||||
|
if (submitting) {
|
||||||
|
cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting = true;
|
||||||
|
isOpen = false;
|
||||||
|
|
||||||
|
return async ({ update }) => {
|
||||||
|
await update({
|
||||||
|
invalidateAll: true,
|
||||||
|
reset: true
|
||||||
|
});
|
||||||
|
|
||||||
|
submitting = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog.Root bind:open={isOpen}>
|
||||||
|
<Dialog.Trigger {...props}>
|
||||||
|
{@render children?.()}
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Name your Playlist</Dialog.Title>
|
||||||
|
<Dialog.Description>Make it memorable</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
|
||||||
|
<form method="POST" action="/playlists?/create" use:enhance={createPlaylist}>
|
||||||
|
<Input
|
||||||
|
name="name"
|
||||||
|
placeholder="My awesome playlist"
|
||||||
|
minlength={1}
|
||||||
|
maxlength={24}
|
||||||
|
bind:value={name}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
class="mt-2 w-full"
|
||||||
|
disabled={submitting || name.length < 1 || name.length > 24}
|
||||||
|
>
|
||||||
|
<CirclePlus />
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
@@ -52,10 +52,6 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
async function skipToQueueIndex(index: number) {
|
|
||||||
console.log(`SKIP TO QUEUE INDEX ${index}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitPlayerAction: SubmitFunction = async () => {
|
const submitPlayerAction: SubmitFunction = async () => {
|
||||||
return async ({ update, result }) => {
|
return async ({ update, result }) => {
|
||||||
await update({
|
await update({
|
||||||
|
|||||||
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>
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
import { deserialize, enhance } from '$app/forms';
|
import { deserialize, enhance } from '$app/forms';
|
||||||
import { getCoverUrl } from '$lib/covers';
|
import { getCoverUrl } from '$lib/covers';
|
||||||
import { getPlayerState } from '$lib/player.svelte';
|
import { getPlayerState } from '$lib/player.svelte';
|
||||||
import { flip } from 'svelte/animate';
|
|
||||||
import type { SubmitFunction } from '../../../routes/player/$types';
|
import type { SubmitFunction } from '../../../routes/player/$types';
|
||||||
import type { Track } from '$lib/proto/library';
|
import type { Track } from '$lib/proto/library';
|
||||||
|
import { ScrollArea } from '$lib/components/ui/scroll-area';
|
||||||
|
|
||||||
const player = getPlayerState();
|
const player = getPlayerState();
|
||||||
|
|
||||||
@@ -63,37 +63,35 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form class="flex w-full flex-col overflow-y-hidden" method="POST" use:enhance={submitPlayerAction}>
|
||||||
class="flex h-full flex-col gap-1 overflow-y-auto"
|
<ScrollArea class="flex h-full w-full flex-col pr-3">
|
||||||
method="POST"
|
{#each player.queue as track, i}
|
||||||
use:enhance={submitPlayerAction}
|
<button
|
||||||
>
|
type="submit"
|
||||||
{#each player.queue as track, i}
|
class="flex w-full flex-row items-center gap-2 rounded-lg px-3 py-2 transition-all hover:bg-secondary"
|
||||||
<button
|
formaction="/player?/skip-to-queue-index&index={i}"
|
||||||
type="submit"
|
draggable={true}
|
||||||
class="flex flex-row items-center gap-2 rounded-lg px-3 py-2 transition-all hover:bg-secondary"
|
ondragstart={onDragStart}
|
||||||
formaction="/player?/skip-to-queue-index&index={i}"
|
ondrop={onDrop}
|
||||||
draggable={true}
|
ondragover={(e) => e.preventDefault()}
|
||||||
ondragstart={onDragStart}
|
data-queue-index={i}
|
||||||
ondrop={onDrop}
|
>
|
||||||
ondragover={(e) => e.preventDefault()}
|
<div class="min-w-8 overflow-hidden rounded-md">
|
||||||
data-queue-index={i}
|
<img src={getCoverUrl(track.hash)} class="aspect-square size-8" alt="Cover" />
|
||||||
>
|
</div>
|
||||||
<div class="min-w-8 overflow-hidden rounded-md">
|
<div class="flex flex-col overflow-hidden">
|
||||||
<img src={getCoverUrl(track.hash)} class="aspect-square size-8" alt="Cover" />
|
<p
|
||||||
</div>
|
class="w-full self-start overflow-hidden text-ellipsis text-nowrap text-left text-sm text-foreground/80"
|
||||||
<div class="flex flex-col overflow-hidden">
|
>
|
||||||
<p
|
{track.name}
|
||||||
class="w-full self-start overflow-hidden text-ellipsis text-nowrap text-left text-sm text-foreground/80"
|
</p>
|
||||||
>
|
<p class="w-full self-start overflow-hidden text-left text-xs text-muted-foreground">
|
||||||
{track.name}
|
{track.artistName}
|
||||||
</p>
|
</p>
|
||||||
<p class="w-full self-start overflow-hidden text-left text-xs text-muted-foreground">
|
</div>
|
||||||
{track.artistName}
|
</button>
|
||||||
</p>
|
{/each}
|
||||||
</div>
|
</ScrollArea>
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
24
src/lib/components/ui/accordion/accordion-content.svelte
Normal file
24
src/lib/components/ui/accordion/accordion-content.svelte
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive, type WithoutChild } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<div class="pb-4 pt-0">
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
12
src/lib/components/ui/accordion/accordion-item.svelte
Normal file
12
src/lib/components/ui/accordion/accordion-item.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: AccordionPrimitive.ItemProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Item bind:ref class={cn("border-b", className)} {...restProps} />
|
||||||
31
src/lib/components/ui/accordion/accordion-trigger.svelte
Normal file
31
src/lib/components/ui/accordion/accordion-trigger.svelte
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Accordion as AccordionPrimitive, type WithoutChild } from "bits-ui";
|
||||||
|
import ChevronDown from "lucide-svelte/icons/chevron-down";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
level = 3,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
|
||||||
|
level?: AccordionPrimitive.HeaderProps["level"];
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AccordionPrimitive.Header {level} class="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ChevronDown
|
||||||
|
class="text-muted-foreground size-4 shrink-0 transition-transform duration-200"
|
||||||
|
/>
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
17
src/lib/components/ui/accordion/index.ts
Normal file
17
src/lib/components/ui/accordion/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
|
import Content from "./accordion-content.svelte";
|
||||||
|
import Item from "./accordion-item.svelte";
|
||||||
|
import Trigger from "./accordion-trigger.svelte";
|
||||||
|
|
||||||
|
const Root = AccordionPrimitive.Root;
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Item,
|
||||||
|
Trigger,
|
||||||
|
//
|
||||||
|
Root as Accordion,
|
||||||
|
Content as AccordionContent,
|
||||||
|
Item as AccordionItem,
|
||||||
|
Trigger as AccordionTrigger,
|
||||||
|
};
|
||||||
16
src/lib/components/ui/card/card-content.svelte
Normal file
16
src/lib/components/ui/card/card-content.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={ref} class={cn("p-6", className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
16
src/lib/components/ui/card/card-description.svelte
Normal file
16
src/lib/components/ui/card/card-description.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p bind:this={ref} class={cn("text-muted-foreground text-sm", className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</p>
|
||||||
16
src/lib/components/ui/card/card-footer.svelte
Normal file
16
src/lib/components/ui/card/card-footer.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={ref} class={cn("flex items-center p-6 pt-0", className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
16
src/lib/components/ui/card/card-header.svelte
Normal file
16
src/lib/components/ui/card/card-header.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={ref} class={cn("flex flex-col space-y-1.5 p-6 pb-0", className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
25
src/lib/components/ui/card/card-title.svelte
Normal file
25
src/lib/components/ui/card/card-title.svelte
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
level = 3,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="heading"
|
||||||
|
aria-level={level}
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn("font-semibold leading-none tracking-tight", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
20
src/lib/components/ui/card/card.svelte
Normal file
20
src/lib/components/ui/card/card.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn("bg-card text-card-foreground rounded-xl border shadow", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
22
src/lib/components/ui/card/index.ts
Normal file
22
src/lib/components/ui/card/index.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import Root from "./card.svelte";
|
||||||
|
import Content from "./card-content.svelte";
|
||||||
|
import Description from "./card-description.svelte";
|
||||||
|
import Footer from "./card-footer.svelte";
|
||||||
|
import Header from "./card-header.svelte";
|
||||||
|
import Title from "./card-title.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Title,
|
||||||
|
//
|
||||||
|
Root as Card,
|
||||||
|
Content as CardContent,
|
||||||
|
Description as CardDescription,
|
||||||
|
Footer as CardFooter,
|
||||||
|
Header as CardHeader,
|
||||||
|
Title as CardTitle,
|
||||||
|
};
|
||||||
38
src/lib/components/ui/dialog/dialog-content.svelte
Normal file
38
src/lib/components/ui/dialog/dialog-content.svelte
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||||
|
import X from "lucide-svelte/icons/x";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import * as Dialog from "./index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
portalProps,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
|
||||||
|
portalProps?: DialogPrimitive.PortalProps;
|
||||||
|
children: Snippet;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog.Portal {...portalProps}>
|
||||||
|
<Dialog.Overlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
class="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
<X class="size-4" />
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
16
src/lib/components/ui/dialog/dialog-description.svelte
Normal file
16
src/lib/components/ui/dialog/dialog-description.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.DescriptionProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
bind:ref
|
||||||
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
19
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
19
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.OverlayProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
bind:ref
|
||||||
|
class={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
16
src/lib/components/ui/dialog/dialog-title.svelte
Normal file
16
src/lib/components/ui/dialog/dialog-title.svelte
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: DialogPrimitive.TitleProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
bind:ref
|
||||||
|
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
37
src/lib/components/ui/dialog/index.ts
Normal file
37
src/lib/components/ui/dialog/index.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
import Title from "./dialog-title.svelte";
|
||||||
|
import Footer from "./dialog-footer.svelte";
|
||||||
|
import Header from "./dialog-header.svelte";
|
||||||
|
import Overlay from "./dialog-overlay.svelte";
|
||||||
|
import Content from "./dialog-content.svelte";
|
||||||
|
import Description from "./dialog-description.svelte";
|
||||||
|
|
||||||
|
const Root: typeof DialogPrimitive.Root = DialogPrimitive.Root;
|
||||||
|
const Trigger: typeof DialogPrimitive.Trigger = DialogPrimitive.Trigger;
|
||||||
|
const Close: typeof DialogPrimitive.Close = DialogPrimitive.Close;
|
||||||
|
const Portal: typeof DialogPrimitive.Portal = DialogPrimitive.Portal;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Title,
|
||||||
|
Portal,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Trigger,
|
||||||
|
Overlay,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
Close,
|
||||||
|
//
|
||||||
|
Root as Dialog,
|
||||||
|
Title as DialogTitle,
|
||||||
|
Portal as DialogPortal,
|
||||||
|
Footer as DialogFooter,
|
||||||
|
Header as DialogHeader,
|
||||||
|
Trigger as DialogTrigger,
|
||||||
|
Overlay as DialogOverlay,
|
||||||
|
Content as DialogContent,
|
||||||
|
Description as DialogDescription,
|
||||||
|
Close as DialogClose,
|
||||||
|
};
|
||||||
24
src/lib/library.svelte.ts
Normal file
24
src/lib/library.svelte.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { getContext, setContext } from 'svelte';
|
||||||
|
import { Playlist, type Track } from './proto/library';
|
||||||
|
|
||||||
|
// TODO: Optimize this since storing entire tracks in
|
||||||
|
// playlists is redundant if we store all the tracks
|
||||||
|
class LibraryState {
|
||||||
|
tracks = $state<Track[]>([]);
|
||||||
|
playlists = $state<Playlist[]>([]);
|
||||||
|
|
||||||
|
constructor(tracks: Track[], playlists: Playlist[]) {
|
||||||
|
this.tracks = tracks;
|
||||||
|
this.playlists = playlists;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LIBRARY_KEY = Symbol('GROOVE_LIBRARY');
|
||||||
|
|
||||||
|
export function setLibraryState(tracks: Track[], playlists: Playlist[]) {
|
||||||
|
return setContext(LIBRARY_KEY, new LibraryState(tracks, playlists));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLibraryState() {
|
||||||
|
return getContext<ReturnType<typeof setLibraryState>>(LIBRARY_KEY);
|
||||||
|
}
|
||||||
@@ -4,6 +4,12 @@
|
|||||||
import type { RpcTransport } from "@protobuf-ts/runtime-rpc";
|
import type { RpcTransport } from "@protobuf-ts/runtime-rpc";
|
||||||
import type { ServiceInfo } from "@protobuf-ts/runtime-rpc";
|
import type { ServiceInfo } from "@protobuf-ts/runtime-rpc";
|
||||||
import { Library } from "./library";
|
import { Library } from "./library";
|
||||||
|
import type { RemoveTrackFromPlaylistRequest } from "./library";
|
||||||
|
import type { AddTrackToPlaylistRequest } from "./library";
|
||||||
|
import type { DeletePlaylistRequest } from "./library";
|
||||||
|
import type { CreatePlaylistResponse } from "./library";
|
||||||
|
import type { CreatePlaylistRequest } from "./library";
|
||||||
|
import type { ListPlaylistsResponse } from "./library";
|
||||||
import { stackIntercept } from "@protobuf-ts/runtime-rpc";
|
import { stackIntercept } from "@protobuf-ts/runtime-rpc";
|
||||||
import type { TrackList } from "./library";
|
import type { TrackList } from "./library";
|
||||||
import type { Empty } from "./google/protobuf/empty";
|
import type { Empty } from "./google/protobuf/empty";
|
||||||
@@ -17,6 +23,26 @@ export interface ILibraryClient {
|
|||||||
* @generated from protobuf rpc: ListTracks(google.protobuf.Empty) returns (library.TrackList);
|
* @generated from protobuf rpc: ListTracks(google.protobuf.Empty) returns (library.TrackList);
|
||||||
*/
|
*/
|
||||||
listTracks(input: Empty, options?: RpcOptions): UnaryCall<Empty, TrackList>;
|
listTracks(input: Empty, options?: RpcOptions): UnaryCall<Empty, TrackList>;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: ListPlaylists(google.protobuf.Empty) returns (library.ListPlaylistsResponse);
|
||||||
|
*/
|
||||||
|
listPlaylists(input: Empty, options?: RpcOptions): UnaryCall<Empty, ListPlaylistsResponse>;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: CreatePlaylist(library.CreatePlaylistRequest) returns (library.CreatePlaylistResponse);
|
||||||
|
*/
|
||||||
|
createPlaylist(input: CreatePlaylistRequest, options?: RpcOptions): UnaryCall<CreatePlaylistRequest, CreatePlaylistResponse>;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: DeletePlaylist(library.DeletePlaylistRequest) returns (google.protobuf.Empty);
|
||||||
|
*/
|
||||||
|
deletePlaylist(input: DeletePlaylistRequest, options?: RpcOptions): UnaryCall<DeletePlaylistRequest, Empty>;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: AddTrackToPlaylist(library.AddTrackToPlaylistRequest) returns (google.protobuf.Empty);
|
||||||
|
*/
|
||||||
|
addTrackToPlaylist(input: AddTrackToPlaylistRequest, options?: RpcOptions): UnaryCall<AddTrackToPlaylistRequest, Empty>;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: RemoveTrackFromPlaylist(library.RemoveTrackFromPlaylistRequest) returns (google.protobuf.Empty);
|
||||||
|
*/
|
||||||
|
removeTrackFromPlaylist(input: RemoveTrackFromPlaylistRequest, options?: RpcOptions): UnaryCall<RemoveTrackFromPlaylistRequest, Empty>;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf service library.Library
|
* @generated from protobuf service library.Library
|
||||||
@@ -34,4 +60,39 @@ export class LibraryClient implements ILibraryClient, ServiceInfo {
|
|||||||
const method = this.methods[0], opt = this._transport.mergeOptions(options);
|
const method = this.methods[0], opt = this._transport.mergeOptions(options);
|
||||||
return stackIntercept<Empty, TrackList>("unary", this._transport, method, opt, input);
|
return stackIntercept<Empty, TrackList>("unary", this._transport, method, opt, input);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: ListPlaylists(google.protobuf.Empty) returns (library.ListPlaylistsResponse);
|
||||||
|
*/
|
||||||
|
listPlaylists(input: Empty, options?: RpcOptions): UnaryCall<Empty, ListPlaylistsResponse> {
|
||||||
|
const method = this.methods[1], opt = this._transport.mergeOptions(options);
|
||||||
|
return stackIntercept<Empty, ListPlaylistsResponse>("unary", this._transport, method, opt, input);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: CreatePlaylist(library.CreatePlaylistRequest) returns (library.CreatePlaylistResponse);
|
||||||
|
*/
|
||||||
|
createPlaylist(input: CreatePlaylistRequest, options?: RpcOptions): UnaryCall<CreatePlaylistRequest, CreatePlaylistResponse> {
|
||||||
|
const method = this.methods[2], opt = this._transport.mergeOptions(options);
|
||||||
|
return stackIntercept<CreatePlaylistRequest, CreatePlaylistResponse>("unary", this._transport, method, opt, input);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: DeletePlaylist(library.DeletePlaylistRequest) returns (google.protobuf.Empty);
|
||||||
|
*/
|
||||||
|
deletePlaylist(input: DeletePlaylistRequest, options?: RpcOptions): UnaryCall<DeletePlaylistRequest, Empty> {
|
||||||
|
const method = this.methods[3], opt = this._transport.mergeOptions(options);
|
||||||
|
return stackIntercept<DeletePlaylistRequest, Empty>("unary", this._transport, method, opt, input);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: AddTrackToPlaylist(library.AddTrackToPlaylistRequest) returns (google.protobuf.Empty);
|
||||||
|
*/
|
||||||
|
addTrackToPlaylist(input: AddTrackToPlaylistRequest, options?: RpcOptions): UnaryCall<AddTrackToPlaylistRequest, Empty> {
|
||||||
|
const method = this.methods[4], opt = this._transport.mergeOptions(options);
|
||||||
|
return stackIntercept<AddTrackToPlaylistRequest, Empty>("unary", this._transport, method, opt, input);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: RemoveTrackFromPlaylist(library.RemoveTrackFromPlaylistRequest) returns (google.protobuf.Empty);
|
||||||
|
*/
|
||||||
|
removeTrackFromPlaylist(input: RemoveTrackFromPlaylistRequest, options?: RpcOptions): UnaryCall<RemoveTrackFromPlaylistRequest, Empty> {
|
||||||
|
const method = this.methods[5], opt = this._transport.mergeOptions(options);
|
||||||
|
return stackIntercept<RemoveTrackFromPlaylistRequest, Empty>("unary", this._transport, method, opt, input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,85 @@ export interface Track {
|
|||||||
*/
|
*/
|
||||||
duration: bigint;
|
duration: bigint;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message library.Playlist
|
||||||
|
*/
|
||||||
|
export interface Playlist {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint32 id = 1;
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string name = 2;
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: repeated library.Track tracks = 3;
|
||||||
|
*/
|
||||||
|
tracks: Track[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message library.ListPlaylistsResponse
|
||||||
|
*/
|
||||||
|
export interface ListPlaylistsResponse {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: repeated library.Playlist playlists = 1;
|
||||||
|
*/
|
||||||
|
playlists: Playlist[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message library.CreatePlaylistRequest
|
||||||
|
*/
|
||||||
|
export interface CreatePlaylistRequest {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string name = 1;
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message library.CreatePlaylistResponse
|
||||||
|
*/
|
||||||
|
export interface CreatePlaylistResponse {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: library.Playlist playlist = 1;
|
||||||
|
*/
|
||||||
|
playlist?: Playlist;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message library.DeletePlaylistRequest
|
||||||
|
*/
|
||||||
|
export interface DeletePlaylistRequest {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint32 id = 1;
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message library.AddTrackToPlaylistRequest
|
||||||
|
*/
|
||||||
|
export interface AddTrackToPlaylistRequest {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint32 playlist_id = 1;
|
||||||
|
*/
|
||||||
|
playlistId: number;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string track_hash = 2;
|
||||||
|
*/
|
||||||
|
trackHash: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message library.RemoveTrackFromPlaylistRequest
|
||||||
|
*/
|
||||||
|
export interface RemoveTrackFromPlaylistRequest {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint32 playlist_id = 1;
|
||||||
|
*/
|
||||||
|
playlistId: number;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint32 track_rank = 2;
|
||||||
|
*/
|
||||||
|
trackRank: number;
|
||||||
|
}
|
||||||
// @generated message type with reflection information, may provide speed optimized methods
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
class TrackList$Type extends MessageType<TrackList> {
|
class TrackList$Type extends MessageType<TrackList> {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -172,9 +251,374 @@ class Track$Type extends MessageType<Track> {
|
|||||||
* @generated MessageType for protobuf message library.Track
|
* @generated MessageType for protobuf message library.Track
|
||||||
*/
|
*/
|
||||||
export const Track = new Track$Type();
|
export const Track = new Track$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class Playlist$Type extends MessageType<Playlist> {
|
||||||
|
constructor() {
|
||||||
|
super("library.Playlist", [
|
||||||
|
{ no: 1, name: "id", kind: "scalar", T: 13 /*ScalarType.UINT32*/ },
|
||||||
|
{ no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 3, name: "tracks", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => Track }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<Playlist>): Playlist {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.id = 0;
|
||||||
|
message.name = "";
|
||||||
|
message.tracks = [];
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<Playlist>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Playlist): Playlist {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* uint32 id */ 1:
|
||||||
|
message.id = reader.uint32();
|
||||||
|
break;
|
||||||
|
case /* string name */ 2:
|
||||||
|
message.name = reader.string();
|
||||||
|
break;
|
||||||
|
case /* repeated library.Track tracks */ 3:
|
||||||
|
message.tracks.push(Track.internalBinaryRead(reader, reader.uint32(), options));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: Playlist, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* uint32 id = 1; */
|
||||||
|
if (message.id !== 0)
|
||||||
|
writer.tag(1, WireType.Varint).uint32(message.id);
|
||||||
|
/* string name = 2; */
|
||||||
|
if (message.name !== "")
|
||||||
|
writer.tag(2, WireType.LengthDelimited).string(message.name);
|
||||||
|
/* repeated library.Track tracks = 3; */
|
||||||
|
for (let i = 0; i < message.tracks.length; i++)
|
||||||
|
Track.internalBinaryWrite(message.tracks[i], writer.tag(3, WireType.LengthDelimited).fork(), options).join();
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message library.Playlist
|
||||||
|
*/
|
||||||
|
export const Playlist = new Playlist$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class ListPlaylistsResponse$Type extends MessageType<ListPlaylistsResponse> {
|
||||||
|
constructor() {
|
||||||
|
super("library.ListPlaylistsResponse", [
|
||||||
|
{ no: 1, name: "playlists", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => Playlist }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<ListPlaylistsResponse>): ListPlaylistsResponse {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.playlists = [];
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<ListPlaylistsResponse>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: ListPlaylistsResponse): ListPlaylistsResponse {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* repeated library.Playlist playlists */ 1:
|
||||||
|
message.playlists.push(Playlist.internalBinaryRead(reader, reader.uint32(), options));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: ListPlaylistsResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* repeated library.Playlist playlists = 1; */
|
||||||
|
for (let i = 0; i < message.playlists.length; i++)
|
||||||
|
Playlist.internalBinaryWrite(message.playlists[i], writer.tag(1, WireType.LengthDelimited).fork(), options).join();
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message library.ListPlaylistsResponse
|
||||||
|
*/
|
||||||
|
export const ListPlaylistsResponse = new ListPlaylistsResponse$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class CreatePlaylistRequest$Type extends MessageType<CreatePlaylistRequest> {
|
||||||
|
constructor() {
|
||||||
|
super("library.CreatePlaylistRequest", [
|
||||||
|
{ no: 1, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<CreatePlaylistRequest>): CreatePlaylistRequest {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.name = "";
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<CreatePlaylistRequest>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: CreatePlaylistRequest): CreatePlaylistRequest {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* string name */ 1:
|
||||||
|
message.name = reader.string();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: CreatePlaylistRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* string name = 1; */
|
||||||
|
if (message.name !== "")
|
||||||
|
writer.tag(1, WireType.LengthDelimited).string(message.name);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message library.CreatePlaylistRequest
|
||||||
|
*/
|
||||||
|
export const CreatePlaylistRequest = new CreatePlaylistRequest$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class CreatePlaylistResponse$Type extends MessageType<CreatePlaylistResponse> {
|
||||||
|
constructor() {
|
||||||
|
super("library.CreatePlaylistResponse", [
|
||||||
|
{ no: 1, name: "playlist", kind: "message", T: () => Playlist }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<CreatePlaylistResponse>): CreatePlaylistResponse {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<CreatePlaylistResponse>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: CreatePlaylistResponse): CreatePlaylistResponse {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* library.Playlist playlist */ 1:
|
||||||
|
message.playlist = Playlist.internalBinaryRead(reader, reader.uint32(), options, message.playlist);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: CreatePlaylistResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* library.Playlist playlist = 1; */
|
||||||
|
if (message.playlist)
|
||||||
|
Playlist.internalBinaryWrite(message.playlist, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message library.CreatePlaylistResponse
|
||||||
|
*/
|
||||||
|
export const CreatePlaylistResponse = new CreatePlaylistResponse$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class DeletePlaylistRequest$Type extends MessageType<DeletePlaylistRequest> {
|
||||||
|
constructor() {
|
||||||
|
super("library.DeletePlaylistRequest", [
|
||||||
|
{ no: 1, name: "id", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<DeletePlaylistRequest>): DeletePlaylistRequest {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.id = 0;
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<DeletePlaylistRequest>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeletePlaylistRequest): DeletePlaylistRequest {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* uint32 id */ 1:
|
||||||
|
message.id = reader.uint32();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: DeletePlaylistRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* uint32 id = 1; */
|
||||||
|
if (message.id !== 0)
|
||||||
|
writer.tag(1, WireType.Varint).uint32(message.id);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message library.DeletePlaylistRequest
|
||||||
|
*/
|
||||||
|
export const DeletePlaylistRequest = new DeletePlaylistRequest$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class AddTrackToPlaylistRequest$Type extends MessageType<AddTrackToPlaylistRequest> {
|
||||||
|
constructor() {
|
||||||
|
super("library.AddTrackToPlaylistRequest", [
|
||||||
|
{ no: 1, name: "playlist_id", kind: "scalar", T: 13 /*ScalarType.UINT32*/ },
|
||||||
|
{ no: 2, name: "track_hash", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<AddTrackToPlaylistRequest>): AddTrackToPlaylistRequest {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.playlistId = 0;
|
||||||
|
message.trackHash = "";
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<AddTrackToPlaylistRequest>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: AddTrackToPlaylistRequest): AddTrackToPlaylistRequest {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* uint32 playlist_id */ 1:
|
||||||
|
message.playlistId = reader.uint32();
|
||||||
|
break;
|
||||||
|
case /* string track_hash */ 2:
|
||||||
|
message.trackHash = reader.string();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: AddTrackToPlaylistRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* uint32 playlist_id = 1; */
|
||||||
|
if (message.playlistId !== 0)
|
||||||
|
writer.tag(1, WireType.Varint).uint32(message.playlistId);
|
||||||
|
/* string track_hash = 2; */
|
||||||
|
if (message.trackHash !== "")
|
||||||
|
writer.tag(2, WireType.LengthDelimited).string(message.trackHash);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message library.AddTrackToPlaylistRequest
|
||||||
|
*/
|
||||||
|
export const AddTrackToPlaylistRequest = new AddTrackToPlaylistRequest$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class RemoveTrackFromPlaylistRequest$Type extends MessageType<RemoveTrackFromPlaylistRequest> {
|
||||||
|
constructor() {
|
||||||
|
super("library.RemoveTrackFromPlaylistRequest", [
|
||||||
|
{ no: 1, name: "playlist_id", kind: "scalar", T: 13 /*ScalarType.UINT32*/ },
|
||||||
|
{ no: 2, name: "track_rank", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<RemoveTrackFromPlaylistRequest>): RemoveTrackFromPlaylistRequest {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.playlistId = 0;
|
||||||
|
message.trackRank = 0;
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<RemoveTrackFromPlaylistRequest>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: RemoveTrackFromPlaylistRequest): RemoveTrackFromPlaylistRequest {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* uint32 playlist_id */ 1:
|
||||||
|
message.playlistId = reader.uint32();
|
||||||
|
break;
|
||||||
|
case /* uint32 track_rank */ 2:
|
||||||
|
message.trackRank = reader.uint32();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: RemoveTrackFromPlaylistRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* uint32 playlist_id = 1; */
|
||||||
|
if (message.playlistId !== 0)
|
||||||
|
writer.tag(1, WireType.Varint).uint32(message.playlistId);
|
||||||
|
/* uint32 track_rank = 2; */
|
||||||
|
if (message.trackRank !== 0)
|
||||||
|
writer.tag(2, WireType.Varint).uint32(message.trackRank);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message library.RemoveTrackFromPlaylistRequest
|
||||||
|
*/
|
||||||
|
export const RemoveTrackFromPlaylistRequest = new RemoveTrackFromPlaylistRequest$Type();
|
||||||
/**
|
/**
|
||||||
* @generated ServiceType for protobuf service library.Library
|
* @generated ServiceType for protobuf service library.Library
|
||||||
*/
|
*/
|
||||||
export const Library = new ServiceType("library.Library", [
|
export const Library = new ServiceType("library.Library", [
|
||||||
{ name: "ListTracks", options: {}, I: Empty, O: TrackList }
|
{ name: "ListTracks", options: {}, I: Empty, O: TrackList },
|
||||||
|
{ name: "ListPlaylists", options: {}, I: Empty, O: ListPlaylistsResponse },
|
||||||
|
{ name: "CreatePlaylist", options: {}, I: CreatePlaylistRequest, O: CreatePlaylistResponse },
|
||||||
|
{ name: "DeletePlaylist", options: {}, I: DeletePlaylistRequest, O: Empty },
|
||||||
|
{ name: "AddTrackToPlaylist", options: {}, I: AddTrackToPlaylistRequest, O: Empty },
|
||||||
|
{ name: "RemoveTrackFromPlaylist", options: {}, I: RemoveTrackFromPlaylistRequest, O: Empty }
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import type { ServiceInfo } from "@protobuf-ts/runtime-rpc";
|
|||||||
import { Player } from "./player";
|
import { Player } from "./player";
|
||||||
import type { SkipToQueueIndexRequest } from "./player";
|
import type { SkipToQueueIndexRequest } from "./player";
|
||||||
import type { SwapQueueIndicesRequest } from "./player";
|
import type { SwapQueueIndicesRequest } from "./player";
|
||||||
|
import type { PlayPlaylistRequest } from "./player";
|
||||||
|
import type { TracksRequest } from "./player";
|
||||||
import type { Queue } from "./player";
|
import type { Queue } from "./player";
|
||||||
import type { SetVolumeResponse } from "./player";
|
import type { SetVolumeResponse } from "./player";
|
||||||
import type { SetVolumeRequest } from "./player";
|
import type { SetVolumeRequest } from "./player";
|
||||||
@@ -60,6 +62,14 @@ export interface IPlayerClient {
|
|||||||
* @generated from protobuf rpc: AddTrackToQueue(player.TrackRequest) returns (player.Queue);
|
* @generated from protobuf rpc: AddTrackToQueue(player.TrackRequest) returns (player.Queue);
|
||||||
*/
|
*/
|
||||||
addTrackToQueue(input: TrackRequest, options?: RpcOptions): UnaryCall<TrackRequest, Queue>;
|
addTrackToQueue(input: TrackRequest, options?: RpcOptions): UnaryCall<TrackRequest, Queue>;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: AddTracksToQueue(player.TracksRequest) returns (player.Queue);
|
||||||
|
*/
|
||||||
|
addTracksToQueue(input: TracksRequest, options?: RpcOptions): UnaryCall<TracksRequest, Queue>;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: PlayPlaylist(player.PlayPlaylistRequest) returns (player.PlayerStatus);
|
||||||
|
*/
|
||||||
|
playPlaylist(input: PlayPlaylistRequest, options?: RpcOptions): UnaryCall<PlayPlaylistRequest, PlayerStatus>;
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: SwapQueueIndices(player.SwapQueueIndicesRequest) returns (player.Queue);
|
* @generated from protobuf rpc: SwapQueueIndices(player.SwapQueueIndicesRequest) returns (player.Queue);
|
||||||
*/
|
*/
|
||||||
@@ -145,25 +155,39 @@ export class PlayerClient implements IPlayerClient, ServiceInfo {
|
|||||||
const method = this.methods[8], opt = this._transport.mergeOptions(options);
|
const method = this.methods[8], opt = this._transport.mergeOptions(options);
|
||||||
return stackIntercept<TrackRequest, Queue>("unary", this._transport, method, opt, input);
|
return stackIntercept<TrackRequest, Queue>("unary", this._transport, method, opt, input);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: AddTracksToQueue(player.TracksRequest) returns (player.Queue);
|
||||||
|
*/
|
||||||
|
addTracksToQueue(input: TracksRequest, options?: RpcOptions): UnaryCall<TracksRequest, Queue> {
|
||||||
|
const method = this.methods[9], opt = this._transport.mergeOptions(options);
|
||||||
|
return stackIntercept<TracksRequest, Queue>("unary", this._transport, method, opt, input);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: PlayPlaylist(player.PlayPlaylistRequest) returns (player.PlayerStatus);
|
||||||
|
*/
|
||||||
|
playPlaylist(input: PlayPlaylistRequest, options?: RpcOptions): UnaryCall<PlayPlaylistRequest, PlayerStatus> {
|
||||||
|
const method = this.methods[10], opt = this._transport.mergeOptions(options);
|
||||||
|
return stackIntercept<PlayPlaylistRequest, PlayerStatus>("unary", this._transport, method, opt, input);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: SwapQueueIndices(player.SwapQueueIndicesRequest) returns (player.Queue);
|
* @generated from protobuf rpc: SwapQueueIndices(player.SwapQueueIndicesRequest) returns (player.Queue);
|
||||||
*/
|
*/
|
||||||
swapQueueIndices(input: SwapQueueIndicesRequest, options?: RpcOptions): UnaryCall<SwapQueueIndicesRequest, Queue> {
|
swapQueueIndices(input: SwapQueueIndicesRequest, options?: RpcOptions): UnaryCall<SwapQueueIndicesRequest, Queue> {
|
||||||
const method = this.methods[9], opt = this._transport.mergeOptions(options);
|
const method = this.methods[11], opt = this._transport.mergeOptions(options);
|
||||||
return stackIntercept<SwapQueueIndicesRequest, Queue>("unary", this._transport, method, opt, input);
|
return stackIntercept<SwapQueueIndicesRequest, Queue>("unary", this._transport, method, opt, input);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: SkipTrack(google.protobuf.Empty) returns (player.PlayerStatus);
|
* @generated from protobuf rpc: SkipTrack(google.protobuf.Empty) returns (player.PlayerStatus);
|
||||||
*/
|
*/
|
||||||
skipTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, PlayerStatus> {
|
skipTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, PlayerStatus> {
|
||||||
const method = this.methods[10], opt = this._transport.mergeOptions(options);
|
const method = this.methods[12], opt = this._transport.mergeOptions(options);
|
||||||
return stackIntercept<Empty, PlayerStatus>("unary", this._transport, method, opt, input);
|
return stackIntercept<Empty, PlayerStatus>("unary", this._transport, method, opt, input);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: SkipToQueueIndex(player.SkipToQueueIndexRequest) returns (player.PlayerStatus);
|
* @generated from protobuf rpc: SkipToQueueIndex(player.SkipToQueueIndexRequest) returns (player.PlayerStatus);
|
||||||
*/
|
*/
|
||||||
skipToQueueIndex(input: SkipToQueueIndexRequest, options?: RpcOptions): UnaryCall<SkipToQueueIndexRequest, PlayerStatus> {
|
skipToQueueIndex(input: SkipToQueueIndexRequest, options?: RpcOptions): UnaryCall<SkipToQueueIndexRequest, PlayerStatus> {
|
||||||
const method = this.methods[11], opt = this._transport.mergeOptions(options);
|
const method = this.methods[13], opt = this._transport.mergeOptions(options);
|
||||||
return stackIntercept<SkipToQueueIndexRequest, PlayerStatus>("unary", this._transport, method, opt, input);
|
return stackIntercept<SkipToQueueIndexRequest, PlayerStatus>("unary", this._transport, method, opt, input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,24 @@ export interface SwapQueueIndicesRequest {
|
|||||||
*/
|
*/
|
||||||
b: number;
|
b: number;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message player.TracksRequest
|
||||||
|
*/
|
||||||
|
export interface TracksRequest {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: repeated string tracks = 1;
|
||||||
|
*/
|
||||||
|
tracks: string[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message player.PlayPlaylistRequest
|
||||||
|
*/
|
||||||
|
export interface PlayPlaylistRequest {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint32 id = 1;
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
// @generated message type with reflection information, may provide speed optimized methods
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
class TrackRequest$Type extends MessageType<TrackRequest> {
|
class TrackRequest$Type extends MessageType<TrackRequest> {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -699,6 +717,100 @@ class SwapQueueIndicesRequest$Type extends MessageType<SwapQueueIndicesRequest>
|
|||||||
* @generated MessageType for protobuf message player.SwapQueueIndicesRequest
|
* @generated MessageType for protobuf message player.SwapQueueIndicesRequest
|
||||||
*/
|
*/
|
||||||
export const SwapQueueIndicesRequest = new SwapQueueIndicesRequest$Type();
|
export const SwapQueueIndicesRequest = new SwapQueueIndicesRequest$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class TracksRequest$Type extends MessageType<TracksRequest> {
|
||||||
|
constructor() {
|
||||||
|
super("player.TracksRequest", [
|
||||||
|
{ no: 1, name: "tracks", kind: "scalar", repeat: 2 /*RepeatType.UNPACKED*/, T: 9 /*ScalarType.STRING*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<TracksRequest>): TracksRequest {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.tracks = [];
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<TracksRequest>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: TracksRequest): TracksRequest {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* repeated string tracks */ 1:
|
||||||
|
message.tracks.push(reader.string());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: TracksRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* repeated string tracks = 1; */
|
||||||
|
for (let i = 0; i < message.tracks.length; i++)
|
||||||
|
writer.tag(1, WireType.LengthDelimited).string(message.tracks[i]);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message player.TracksRequest
|
||||||
|
*/
|
||||||
|
export const TracksRequest = new TracksRequest$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class PlayPlaylistRequest$Type extends MessageType<PlayPlaylistRequest> {
|
||||||
|
constructor() {
|
||||||
|
super("player.PlayPlaylistRequest", [
|
||||||
|
{ no: 1, name: "id", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<PlayPlaylistRequest>): PlayPlaylistRequest {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.id = 0;
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<PlayPlaylistRequest>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: PlayPlaylistRequest): PlayPlaylistRequest {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* uint32 id */ 1:
|
||||||
|
message.id = reader.uint32();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: PlayPlaylistRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* uint32 id = 1; */
|
||||||
|
if (message.id !== 0)
|
||||||
|
writer.tag(1, WireType.Varint).uint32(message.id);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message player.PlayPlaylistRequest
|
||||||
|
*/
|
||||||
|
export const PlayPlaylistRequest = new PlayPlaylistRequest$Type();
|
||||||
/**
|
/**
|
||||||
* @generated ServiceType for protobuf service player.Player
|
* @generated ServiceType for protobuf service player.Player
|
||||||
*/
|
*/
|
||||||
@@ -712,6 +824,8 @@ export const Player = new ServiceType("player.Player", [
|
|||||||
{ name: "SetVolume", options: {}, I: SetVolumeRequest, O: SetVolumeResponse },
|
{ name: "SetVolume", options: {}, I: SetVolumeRequest, O: SetVolumeResponse },
|
||||||
{ name: "PlayTrackNext", options: {}, I: TrackRequest, O: Queue },
|
{ name: "PlayTrackNext", options: {}, I: TrackRequest, O: Queue },
|
||||||
{ name: "AddTrackToQueue", options: {}, I: TrackRequest, O: Queue },
|
{ name: "AddTrackToQueue", options: {}, I: TrackRequest, O: Queue },
|
||||||
|
{ name: "AddTracksToQueue", options: {}, I: TracksRequest, O: Queue },
|
||||||
|
{ name: "PlayPlaylist", options: {}, I: PlayPlaylistRequest, O: PlayerStatus },
|
||||||
{ name: "SwapQueueIndices", options: {}, I: SwapQueueIndicesRequest, O: Queue },
|
{ name: "SwapQueueIndices", options: {}, I: SwapQueueIndicesRequest, O: Queue },
|
||||||
{ name: "SkipTrack", options: {}, I: Empty, O: PlayerStatus },
|
{ name: "SkipTrack", options: {}, I: Empty, O: PlayerStatus },
|
||||||
{ name: "SkipToQueueIndex", options: {}, I: SkipToQueueIndexRequest, O: PlayerStatus }
|
{ name: "SkipToQueueIndex", options: {}, I: SkipToQueueIndexRequest, O: PlayerStatus }
|
||||||
|
|||||||
@@ -1,6 +1,38 @@
|
|||||||
|
import type { Playlist, Track } from '$lib/proto/library';
|
||||||
|
import { LibraryClient } from '$lib/proto/library.client';
|
||||||
|
import { protoTransport } from '../hooks.server';
|
||||||
import type { LayoutServerLoad } from './$types';
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: LayoutServerLoad = async () => {
|
export const load: LayoutServerLoad = async () => {
|
||||||
// const client = new PlayerClient(protoTransport);
|
const client = new LibraryClient(protoTransport);
|
||||||
// TODO: Get current track
|
|
||||||
|
const tracksResponse = await client.listTracks({});
|
||||||
|
const playlistResponse = await client.listPlaylists({});
|
||||||
|
|
||||||
|
// TODO: Extract these mapping functions into a utility function
|
||||||
|
|
||||||
|
const tracks = tracksResponse.response.tracks.map((t) => ({
|
||||||
|
hash: t.hash,
|
||||||
|
name: t.name,
|
||||||
|
artistId: t.artistId,
|
||||||
|
artistName: t.artistName,
|
||||||
|
duration: t.duration
|
||||||
|
}));
|
||||||
|
|
||||||
|
const playlists = playlistResponse.response.playlists.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
tracks: p.tracks.map((t) => ({
|
||||||
|
hash: t.hash,
|
||||||
|
name: t.name,
|
||||||
|
artistId: t.artistId,
|
||||||
|
artistName: t.artistName,
|
||||||
|
duration: t.duration
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
tracks: tracks as Track[],
|
||||||
|
playlists: playlists as Playlist[]
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,16 +6,25 @@
|
|||||||
import Footer from '$lib/components/groove/Footer.svelte';
|
import Footer from '$lib/components/groove/Footer.svelte';
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area';
|
import { ScrollArea } from '$lib/components/ui/scroll-area';
|
||||||
import { getPlayerState, setPlayerState } from '$lib/player.svelte';
|
import { getPlayerState, setPlayerState } from '$lib/player.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Search } from 'lucide-svelte';
|
import { Search } from 'lucide-svelte';
|
||||||
import { setSearchState, getSearchState } from '$lib/search.svelte';
|
import { setSearchState, getSearchState } from '$lib/search.svelte';
|
||||||
|
import { setLibraryState } from '$lib/library.svelte';
|
||||||
|
import type { LayoutServerData } from './$types';
|
||||||
|
|
||||||
let { children } = $props();
|
interface Props {
|
||||||
|
data: LayoutServerData;
|
||||||
|
children: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { data, children }: Props = $props();
|
||||||
|
|
||||||
setPlayerState();
|
setPlayerState();
|
||||||
setSearchState();
|
setSearchState();
|
||||||
|
setLibraryState(data.tracks, data.playlists);
|
||||||
|
|
||||||
const player = getPlayerState();
|
const player = getPlayerState();
|
||||||
const search = getSearchState();
|
const search = getSearchState();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Playlist } from '$lib/proto/library';
|
||||||
|
import { LibraryClient } from '$lib/proto/library.client';
|
||||||
|
import { fail } from '@sveltejs/kit';
|
||||||
|
import { protoTransport } from '../../hooks.server';
|
||||||
|
import type { Actions } from './$types';
|
||||||
|
import { serializable } from '$lib/proto';
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
create: async ({ request }) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
|
||||||
|
const name = formData.get('name')?.toString();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return fail(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new LibraryClient(protoTransport);
|
||||||
|
|
||||||
|
const response = await client.createPlaylist({ name });
|
||||||
|
|
||||||
|
return serializable<Playlist>(response.response);
|
||||||
|
}
|
||||||
|
} satisfies Actions;
|
||||||
|
|||||||
@@ -1 +1,14 @@
|
|||||||
Playlists
|
<script lang="ts">
|
||||||
|
import PlaylistListing from '$lib/components/groove/PlaylistListing.svelte';
|
||||||
|
import { getLibraryState } from '$lib/library.svelte';
|
||||||
|
|
||||||
|
const library = getLibraryState();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-2 gap-4 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-7 2xl:grid-cols-8"
|
||||||
|
>
|
||||||
|
{#each library.playlists as playlist}
|
||||||
|
<PlaylistListing id={playlist.id} name={playlist.name} tracks={playlist.tracks} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|||||||
19
src/routes/playlists/[id]/+page.server.ts
Normal file
19
src/routes/playlists/[id]/+page.server.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Actions } from './$types';
|
||||||
|
import { protoTransport } from '../../../hooks.server';
|
||||||
|
import { PlayerClient } from '$lib/proto/player.client';
|
||||||
|
import { LibraryClient } from '$lib/proto/library.client';
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
play: async ({ params }) => {
|
||||||
|
const client = new PlayerClient(protoTransport);
|
||||||
|
|
||||||
|
const _response = await client.playPlaylist({
|
||||||
|
id: parseInt(params.id)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
delete: async ({ params }) => {
|
||||||
|
const client = new LibraryClient(protoTransport);
|
||||||
|
|
||||||
|
const _response = await client.deletePlaylist({ id: parseInt(params.id) });
|
||||||
|
}
|
||||||
|
} satisfies Actions;
|
||||||
14
src/routes/playlists/[id]/add-track/[hash]/+page.server.ts
Normal file
14
src/routes/playlists/[id]/add-track/[hash]/+page.server.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { LibraryClient } from '$lib/proto/library.client';
|
||||||
|
import { protoTransport } from '../../../../../hooks.server';
|
||||||
|
import type { Actions } from './$types';
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async ({ params }) => {
|
||||||
|
const client = new LibraryClient(protoTransport);
|
||||||
|
|
||||||
|
const _response = await client.addTrackToPlaylist({
|
||||||
|
playlistId: parseInt(params.id),
|
||||||
|
trackHash: params.hash
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} satisfies Actions;
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import type { Track } from '$lib/proto/library';
|
|
||||||
import { LibraryClient } from '$lib/proto/library.client';
|
|
||||||
import { protoTransport } from '../../hooks.server';
|
|
||||||
import type { PageServerLoad } from './$types';
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
|
||||||
const client = new LibraryClient(protoTransport);
|
|
||||||
|
|
||||||
const response = await client.listTracks({});
|
|
||||||
|
|
||||||
const tracks = response.response.tracks.map((t) => ({
|
|
||||||
hash: t.hash,
|
|
||||||
name: t.name,
|
|
||||||
artistId: t.artistId,
|
|
||||||
artistName: t.artistName
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
tracks: tracks as Track[]
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -4,11 +4,15 @@
|
|||||||
import { getPlayerState } from '$lib/player.svelte';
|
import { getPlayerState } from '$lib/player.svelte';
|
||||||
import type { PageServerData } from './$types';
|
import type { PageServerData } from './$types';
|
||||||
import type { SubmitFunction } from './[hash]/$types';
|
import type { SubmitFunction } from './[hash]/$types';
|
||||||
|
import type { SubmitFunction as AddTrackSubmitFunction } from '../playlists/[id]/add-track/[hash]/$types';
|
||||||
import * as ContextMenu from '$lib/components/ui/context-menu';
|
import * as ContextMenu from '$lib/components/ui/context-menu';
|
||||||
import Music2 from 'virtual:icons/lucide/music-2';
|
import Music2 from 'virtual:icons/lucide/music-2';
|
||||||
import ListMusic from 'virtual:icons/lucide/list-music';
|
import ListMusic from 'virtual:icons/lucide/list-music';
|
||||||
import ListEnd from 'virtual:icons/lucide/list-end';
|
import ListEnd from 'virtual:icons/lucide/list-end';
|
||||||
|
import ListPlus from 'virtual:icons/lucide/list-plus';
|
||||||
import { getSearchState } from '$lib/search.svelte';
|
import { getSearchState } from '$lib/search.svelte';
|
||||||
|
import { getLibraryState } from '$lib/library.svelte';
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageServerData;
|
data: PageServerData;
|
||||||
@@ -18,9 +22,9 @@
|
|||||||
|
|
||||||
const player = getPlayerState();
|
const player = getPlayerState();
|
||||||
const search = getSearchState();
|
const search = getSearchState();
|
||||||
|
const library = getLibraryState();
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
console.log('set tracks');
|
|
||||||
search.setTracks(data.tracks);
|
search.setTracks(data.tracks);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,6 +61,38 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Optimize this
|
||||||
|
// - reduce indentation
|
||||||
|
// - get rid of the `.find()`s
|
||||||
|
const addTrackToPlaylist: AddTrackSubmitFunction = async ({ action }) => {
|
||||||
|
const playlistId = parseInt(action.pathname.split('/playlists/')[1].split('/')[0]);
|
||||||
|
|
||||||
|
const targetHash = $state.snapshot(contextMenuTarget);
|
||||||
|
|
||||||
|
contextMenuTarget = null;
|
||||||
|
|
||||||
|
return async ({ update, result }) => {
|
||||||
|
await update({
|
||||||
|
invalidateAll: false,
|
||||||
|
reset: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.type === 'success') {
|
||||||
|
const playlist = library.playlists.find((p) => p.id === playlistId);
|
||||||
|
|
||||||
|
if (playlist) {
|
||||||
|
const track = library.tracks.find((t) => t.hash === targetHash);
|
||||||
|
|
||||||
|
if (track) {
|
||||||
|
playlist.tracks.push(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
library.playlists = library.playlists;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
function contextMenuItemClicked(e: MouseEvent) {
|
function contextMenuItemClicked(e: MouseEvent) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
@@ -111,6 +147,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
|
|
||||||
|
<ContextMenu.Separator />
|
||||||
|
|
||||||
<ContextMenu.Item class="!p-0">
|
<ContextMenu.Item class="!p-0">
|
||||||
<form
|
<form
|
||||||
class="flex w-full"
|
class="flex w-full"
|
||||||
@@ -145,6 +184,36 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
|
|
||||||
|
<ContextMenu.Separator />
|
||||||
|
|
||||||
|
<ContextMenu.Sub>
|
||||||
|
<ContextMenu.SubTrigger>
|
||||||
|
<ListPlus class="mr-1 size-4" />
|
||||||
|
Add to Playlist
|
||||||
|
</ContextMenu.SubTrigger>
|
||||||
|
<ContextMenu.SubContent>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<form {...props} method="POST" use:enhance={addTrackToPlaylist}>
|
||||||
|
{#each library.playlists as playlist}
|
||||||
|
<ContextMenu.Item>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
type="submit"
|
||||||
|
onclick={contextMenuItemClicked}
|
||||||
|
formaction="/playlists/{playlist.id}/add-track/{contextMenuTarget!}"
|
||||||
|
class={cn(props.class ?? '', 'w-full')}
|
||||||
|
>
|
||||||
|
{playlist.name}
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
{/each}
|
||||||
|
</form>
|
||||||
|
{/snippet}
|
||||||
|
</ContextMenu.SubContent>
|
||||||
|
</ContextMenu.Sub>
|
||||||
</ContextMenu.Content>
|
</ContextMenu.Content>
|
||||||
</ContextMenu.Root>
|
</ContextMenu.Root>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user