feat(playlists): remove tracks
This commit is contained in:
@@ -9,8 +9,8 @@ service Library {
|
|||||||
rpc ListPlaylists(google.protobuf.Empty) returns (ListPlaylistsResponse);
|
rpc ListPlaylists(google.protobuf.Empty) returns (ListPlaylistsResponse);
|
||||||
rpc CreatePlaylist(CreatePlaylistRequest) returns (CreatePlaylistResponse);
|
rpc CreatePlaylist(CreatePlaylistRequest) returns (CreatePlaylistResponse);
|
||||||
rpc DeletePlaylist(DeletePlaylistRequest) returns (google.protobuf.Empty);
|
rpc DeletePlaylist(DeletePlaylistRequest) returns (google.protobuf.Empty);
|
||||||
rpc AddTrackToPlaylist(AddTrackToPlaylistRequest) returns (google.protobuf.Empty);
|
rpc AddTrackToPlaylist(AddTrackToPlaylistRequest) returns (TrackList);
|
||||||
rpc RemoveTrackFromPlaylist(RemoveTrackFromPlaylistRequest) returns (google.protobuf.Empty);
|
rpc RemoveTrackFromPlaylist(RemoveTrackFromPlaylistRequest) returns (TrackList);
|
||||||
rpc SwapTracks(SwapTracksRequest) returns (TrackList);
|
rpc SwapTracks(SwapTracksRequest) returns (TrackList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</Sidebar.MenuSubButton>
|
</Sidebar.MenuSubButton>
|
||||||
</Sidebar.MenuSubItem>
|
</Sidebar.MenuSubItem>
|
||||||
{#each library.playlists as playlist}
|
{#each library.playlists as [_, playlist]}
|
||||||
{@const playlistLocation = `/playlists/${playlist.id}`}
|
{@const playlistLocation = `/playlists/${playlist.id}`}
|
||||||
<Sidebar.MenuSubItem>
|
<Sidebar.MenuSubItem>
|
||||||
<Sidebar.MenuSubButton
|
<Sidebar.MenuSubButton
|
||||||
|
|||||||
@@ -1,15 +1,107 @@
|
|||||||
import { getContext, setContext } from 'svelte';
|
import { getContext, setContext } from 'svelte';
|
||||||
import { Playlist, type Track } from './proto/library';
|
import { Playlist, TrackList, type Track } from './proto/library';
|
||||||
|
import { deserialize } from '$app/forms';
|
||||||
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
|
||||||
// TODO: Optimize this since storing entire tracks in
|
// TODO: Optimize this since storing entire tracks in
|
||||||
// playlists is redundant if we store all the tracks
|
// playlists is redundant if we store all the tracks
|
||||||
class LibraryState {
|
class LibraryState {
|
||||||
tracks = $state<Track[]>([]);
|
tracks = $state<Track[]>([]);
|
||||||
playlists = $state<Playlist[]>([]);
|
playlists = $state<SvelteMap<number, Playlist>>(new SvelteMap());
|
||||||
|
|
||||||
constructor(tracks: Track[], playlists: Playlist[]) {
|
constructor(tracks: Track[], playlists: Playlist[]) {
|
||||||
this.tracks = tracks;
|
this.tracks = tracks;
|
||||||
this.playlists = playlists;
|
this.playlists = new SvelteMap(playlists.map((p) => [p.id, p]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTrackToPlaylist(playlistId: number, trackHash: string, fetch: typeof globalThis.fetch) {
|
||||||
|
const response = await fetch(`/playlists/${playlistId}/add-track/${trackHash}`, {
|
||||||
|
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 && 'tracks' in result.data) {
|
||||||
|
const playlist = this.playlists.get(playlistId);
|
||||||
|
|
||||||
|
if (playlist) {
|
||||||
|
this.playlists.set(playlistId, {
|
||||||
|
...playlist,
|
||||||
|
tracks: (result.data as unknown as TrackList).tracks
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeTrackFromPlaylist(
|
||||||
|
playlistId: number,
|
||||||
|
trackRank: number,
|
||||||
|
fetch: typeof globalThis.fetch
|
||||||
|
) {
|
||||||
|
const response = await fetch(`/playlists/${playlistId}/remove-track/${trackRank}`, {
|
||||||
|
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 && 'tracks' in result.data) {
|
||||||
|
const playlist = this.playlists.get(playlistId);
|
||||||
|
|
||||||
|
if (playlist) {
|
||||||
|
this.playlists.set(playlistId, {
|
||||||
|
...playlist,
|
||||||
|
tracks: (result.data as unknown as TrackList).tracks
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async swapPlaylistTracks(playlistId: number, aIndex: number, bIndex: number) {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.set('a', aIndex.toString());
|
||||||
|
formData.set('b', bIndex.toString());
|
||||||
|
|
||||||
|
const response = await fetch(`/playlists/${playlistId}?/swap-tracks`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'x-sveltekit-action': 'true'
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = deserialize(await response.text());
|
||||||
|
|
||||||
|
if (result.type === 'success' && result.data && 'tracks' in result.data) {
|
||||||
|
const playlist = this.playlists.get(playlistId);
|
||||||
|
|
||||||
|
if (playlist) {
|
||||||
|
this.playlists.set(playlistId, {
|
||||||
|
...playlist,
|
||||||
|
tracks: (result.data as unknown as TrackList).tracks
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ export interface ILibraryClient {
|
|||||||
*/
|
*/
|
||||||
deletePlaylist(input: DeletePlaylistRequest, options?: RpcOptions): UnaryCall<DeletePlaylistRequest, Empty>;
|
deletePlaylist(input: DeletePlaylistRequest, options?: RpcOptions): UnaryCall<DeletePlaylistRequest, Empty>;
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: AddTrackToPlaylist(library.AddTrackToPlaylistRequest) returns (google.protobuf.Empty);
|
* @generated from protobuf rpc: AddTrackToPlaylist(library.AddTrackToPlaylistRequest) returns (library.TrackList);
|
||||||
*/
|
*/
|
||||||
addTrackToPlaylist(input: AddTrackToPlaylistRequest, options?: RpcOptions): UnaryCall<AddTrackToPlaylistRequest, Empty>;
|
addTrackToPlaylist(input: AddTrackToPlaylistRequest, options?: RpcOptions): UnaryCall<AddTrackToPlaylistRequest, TrackList>;
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: RemoveTrackFromPlaylist(library.RemoveTrackFromPlaylistRequest) returns (google.protobuf.Empty);
|
* @generated from protobuf rpc: RemoveTrackFromPlaylist(library.RemoveTrackFromPlaylistRequest) returns (library.TrackList);
|
||||||
*/
|
*/
|
||||||
removeTrackFromPlaylist(input: RemoveTrackFromPlaylistRequest, options?: RpcOptions): UnaryCall<RemoveTrackFromPlaylistRequest, Empty>;
|
removeTrackFromPlaylist(input: RemoveTrackFromPlaylistRequest, options?: RpcOptions): UnaryCall<RemoveTrackFromPlaylistRequest, TrackList>;
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: SwapTracks(library.SwapTracksRequest) returns (library.TrackList);
|
* @generated from protobuf rpc: SwapTracks(library.SwapTracksRequest) returns (library.TrackList);
|
||||||
*/
|
*/
|
||||||
@@ -87,18 +87,18 @@ export class LibraryClient implements ILibraryClient, ServiceInfo {
|
|||||||
return stackIntercept<DeletePlaylistRequest, Empty>("unary", this._transport, method, opt, input);
|
return stackIntercept<DeletePlaylistRequest, Empty>("unary", this._transport, method, opt, input);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: AddTrackToPlaylist(library.AddTrackToPlaylistRequest) returns (google.protobuf.Empty);
|
* @generated from protobuf rpc: AddTrackToPlaylist(library.AddTrackToPlaylistRequest) returns (library.TrackList);
|
||||||
*/
|
*/
|
||||||
addTrackToPlaylist(input: AddTrackToPlaylistRequest, options?: RpcOptions): UnaryCall<AddTrackToPlaylistRequest, Empty> {
|
addTrackToPlaylist(input: AddTrackToPlaylistRequest, options?: RpcOptions): UnaryCall<AddTrackToPlaylistRequest, TrackList> {
|
||||||
const method = this.methods[4], opt = this._transport.mergeOptions(options);
|
const method = this.methods[4], opt = this._transport.mergeOptions(options);
|
||||||
return stackIntercept<AddTrackToPlaylistRequest, Empty>("unary", this._transport, method, opt, input);
|
return stackIntercept<AddTrackToPlaylistRequest, TrackList>("unary", this._transport, method, opt, input);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: RemoveTrackFromPlaylist(library.RemoveTrackFromPlaylistRequest) returns (google.protobuf.Empty);
|
* @generated from protobuf rpc: RemoveTrackFromPlaylist(library.RemoveTrackFromPlaylistRequest) returns (library.TrackList);
|
||||||
*/
|
*/
|
||||||
removeTrackFromPlaylist(input: RemoveTrackFromPlaylistRequest, options?: RpcOptions): UnaryCall<RemoveTrackFromPlaylistRequest, Empty> {
|
removeTrackFromPlaylist(input: RemoveTrackFromPlaylistRequest, options?: RpcOptions): UnaryCall<RemoveTrackFromPlaylistRequest, TrackList> {
|
||||||
const method = this.methods[5], opt = this._transport.mergeOptions(options);
|
const method = this.methods[5], opt = this._transport.mergeOptions(options);
|
||||||
return stackIntercept<RemoveTrackFromPlaylistRequest, Empty>("unary", this._transport, method, opt, input);
|
return stackIntercept<RemoveTrackFromPlaylistRequest, TrackList>("unary", this._transport, method, opt, input);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: SwapTracks(library.SwapTracksRequest) returns (library.TrackList);
|
* @generated from protobuf rpc: SwapTracks(library.SwapTracksRequest) returns (library.TrackList);
|
||||||
|
|||||||
@@ -699,7 +699,7 @@ export const Library = new ServiceType("library.Library", [
|
|||||||
{ name: "ListPlaylists", options: {}, I: Empty, O: ListPlaylistsResponse },
|
{ name: "ListPlaylists", options: {}, I: Empty, O: ListPlaylistsResponse },
|
||||||
{ name: "CreatePlaylist", options: {}, I: CreatePlaylistRequest, O: CreatePlaylistResponse },
|
{ name: "CreatePlaylist", options: {}, I: CreatePlaylistRequest, O: CreatePlaylistResponse },
|
||||||
{ name: "DeletePlaylist", options: {}, I: DeletePlaylistRequest, O: Empty },
|
{ name: "DeletePlaylist", options: {}, I: DeletePlaylistRequest, O: Empty },
|
||||||
{ name: "AddTrackToPlaylist", options: {}, I: AddTrackToPlaylistRequest, O: Empty },
|
{ name: "AddTrackToPlaylist", options: {}, I: AddTrackToPlaylistRequest, O: TrackList },
|
||||||
{ name: "RemoveTrackFromPlaylist", options: {}, I: RemoveTrackFromPlaylistRequest, O: Empty },
|
{ name: "RemoveTrackFromPlaylist", options: {}, I: RemoveTrackFromPlaylistRequest, O: TrackList },
|
||||||
{ name: "SwapTracks", options: {}, I: SwapTracksRequest, O: TrackList }
|
{ name: "SwapTracks", options: {}, I: SwapTracksRequest, O: TrackList }
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -7,16 +7,16 @@
|
|||||||
import duration from 'dayjs/plugin/duration';
|
import duration from 'dayjs/plugin/duration';
|
||||||
import { getCoverUrl } from '$lib/covers';
|
import { getCoverUrl } from '$lib/covers';
|
||||||
import Play from 'virtual:icons/lucide/play';
|
import Play from 'virtual:icons/lucide/play';
|
||||||
|
import Trash2 from 'virtual:icons/lucide/trash-2';
|
||||||
import { getPlayerState } from '$lib/player.svelte';
|
import { getPlayerState } from '$lib/player.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { deserialize } from '$app/forms';
|
import * as ContextMenu from '$lib/components/ui/context-menu';
|
||||||
import type { TrackList } from '$lib/proto/library';
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
const library = getLibraryState();
|
const library = getLibraryState();
|
||||||
const player = getPlayerState();
|
const player = getPlayerState();
|
||||||
|
|
||||||
let playlist = $derived(library.playlists.find((p) => p.id === Number($page.params.id)));
|
let playlist = $derived(library.playlists.get(Number($page.params.id)));
|
||||||
|
|
||||||
let totalDuration = $derived<number>(
|
let totalDuration = $derived<number>(
|
||||||
playlist?.tracks.reduce((acc, track) => acc + Number(track.duration), 0) ?? 0
|
playlist?.tracks.reduce((acc, track) => acc + Number(track.duration), 0) ?? 0
|
||||||
@@ -29,39 +29,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onDrop(e: DragEvent) {
|
async function onDrop(e: DragEvent) {
|
||||||
const aIndex = e.dataTransfer?.getData('text/plain')!;
|
if (!playlist) {
|
||||||
const bIndex = (e.currentTarget as HTMLElement).getAttribute('data-playlist-index')!;
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
formData.set('a', aIndex);
|
|
||||||
formData.set('b', bIndex);
|
|
||||||
|
|
||||||
const response = await fetch(`/playlists/${playlist!.id}?/swap-tracks`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'x-sveltekit-action': 'true'
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = deserialize(await response.text());
|
const aIndex = e.dataTransfer?.getData('text/plain')!;
|
||||||
|
const bIndex = (e.currentTarget as HTMLElement).getAttribute('data-playlist-index')!;
|
||||||
|
|
||||||
if (result.type === 'success' && result.data && 'tracks' in result.data) {
|
library.swapPlaylistTracks(playlist.id, Number(aIndex), Number(bIndex));
|
||||||
const _playlist = library.playlists.find((p) => p.id === playlist?.id);
|
|
||||||
|
|
||||||
if (playlist) {
|
|
||||||
playlist.tracks = (result.data as unknown as TrackList).tracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_playlist) {
|
|
||||||
_playlist.tracks = (result.data as unknown as TrackList).tracks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -99,27 +74,46 @@
|
|||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
{#each playlist.tracks as track, i}
|
{#each playlist.tracks as track, i}
|
||||||
<Table.Row
|
<ContextMenu.Root>
|
||||||
class="select-none"
|
<ContextMenu.Trigger>
|
||||||
ondblclick={() => player.playTrack(track.hash, fetch)}
|
{#snippet child({ props })}
|
||||||
data-playlist-index={i}
|
<Table.Row
|
||||||
ondragstart={onDragStart}
|
{...props}
|
||||||
ondrop={onDrop}
|
class="select-none"
|
||||||
ondragover={(e) => e.preventDefault()}
|
ondblclick={() => player.playTrack(track.hash, fetch)}
|
||||||
draggable
|
data-playlist-index={i}
|
||||||
>
|
ondragstart={onDragStart}
|
||||||
<Table.Cell class="text-center">{i + 1}</Table.Cell>
|
ondrop={onDrop}
|
||||||
<Table.Cell class="w-0">
|
ondragover={(e) => e.preventDefault()}
|
||||||
<img class="overflow-hidden rounded-md" src={getCoverUrl(track.hash)} alt="" />
|
draggable
|
||||||
</Table.Cell>
|
>
|
||||||
<Table.Cell class="text-nowrap">{track.name}</Table.Cell>
|
<Table.Cell class="text-center">{i + 1}</Table.Cell>
|
||||||
<Table.Cell class="w-fit text-nowrap">{track.artistName}</Table.Cell>
|
<Table.Cell class="w-0">
|
||||||
<Table.Cell class="text-right"
|
<img
|
||||||
>{dayjs
|
class="overflow-hidden rounded-md"
|
||||||
.duration(Number(track.duration), 'milliseconds')
|
src={getCoverUrl(track.hash)}
|
||||||
.format('mm:ss')}</Table.Cell
|
alt=""
|
||||||
>
|
/>
|
||||||
</Table.Row>
|
</Table.Cell>
|
||||||
|
<Table.Cell class="text-nowrap">{track.name}</Table.Cell>
|
||||||
|
<Table.Cell class="w-fit text-nowrap">{track.artistName}</Table.Cell>
|
||||||
|
<Table.Cell class="text-right"
|
||||||
|
>{dayjs
|
||||||
|
.duration(Number(track.duration), 'milliseconds')
|
||||||
|
.format('mm:ss')}</Table.Cell
|
||||||
|
>
|
||||||
|
</Table.Row>
|
||||||
|
{/snippet}
|
||||||
|
</ContextMenu.Trigger>
|
||||||
|
<ContextMenu.Content>
|
||||||
|
<ContextMenu.Item
|
||||||
|
onclick={() => library.removeTrackFromPlaylist(playlist.id, i, fetch)}
|
||||||
|
>
|
||||||
|
<Trash2 class="mr-1 size-4" />
|
||||||
|
Remove from Playlist
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu.Content>
|
||||||
|
</ContextMenu.Root>
|
||||||
{/each}
|
{/each}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
</Table.Root>
|
</Table.Root>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { serializable } from '$lib/proto';
|
||||||
|
import type { TrackList } from '$lib/proto/library';
|
||||||
import { LibraryClient } from '$lib/proto/library.client';
|
import { LibraryClient } from '$lib/proto/library.client';
|
||||||
import { protoTransport } from '../../../../../hooks.server';
|
import { protoTransport } from '../../../../../hooks.server';
|
||||||
import type { Actions } from './$types';
|
import type { Actions } from './$types';
|
||||||
@@ -6,9 +8,11 @@ export const actions = {
|
|||||||
default: async ({ params }) => {
|
default: async ({ params }) => {
|
||||||
const client = new LibraryClient(protoTransport);
|
const client = new LibraryClient(protoTransport);
|
||||||
|
|
||||||
const _response = await client.addTrackToPlaylist({
|
const response = await client.addTrackToPlaylist({
|
||||||
playlistId: parseInt(params.id),
|
playlistId: parseInt(params.id),
|
||||||
trackHash: params.hash
|
trackHash: params.hash
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return serializable<TrackList>(response.response);
|
||||||
}
|
}
|
||||||
} satisfies Actions;
|
} satisfies Actions;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { serializable } from '$lib/proto';
|
||||||
|
import { TrackList } from '$lib/proto/library';
|
||||||
|
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.removeTrackFromPlaylist({
|
||||||
|
playlistId: parseInt(params.id),
|
||||||
|
trackRank: Number(params.rank)
|
||||||
|
});
|
||||||
|
|
||||||
|
return serializable<TrackList>(response.response);
|
||||||
|
}
|
||||||
|
} satisfies Actions;
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
import TrackListing from '$lib/components/groove/TrackListing.svelte';
|
import TrackListing from '$lib/components/groove/TrackListing.svelte';
|
||||||
import { getPlayerState } from '$lib/player.svelte';
|
import { getPlayerState } from '$lib/player.svelte';
|
||||||
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';
|
||||||
@@ -11,7 +10,6 @@
|
|||||||
import ListPlus from 'virtual:icons/lucide/list-plus';
|
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 { getLibraryState } from '$lib/library.svelte';
|
||||||
import { cn } from '$lib/utils';
|
|
||||||
|
|
||||||
const player = getPlayerState();
|
const player = getPlayerState();
|
||||||
const search = getSearchState();
|
const search = getSearchState();
|
||||||
@@ -54,38 +52,6 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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();
|
||||||
}
|
}
|
||||||
@@ -122,22 +88,9 @@
|
|||||||
customAnchor={`div[data-track-hash="${contextMenuTarget!}"]`}
|
customAnchor={`div[data-track-hash="${contextMenuTarget!}"]`}
|
||||||
strategy="fixed"
|
strategy="fixed"
|
||||||
>
|
>
|
||||||
<ContextMenu.Item class="!p-0">
|
<ContextMenu.Item onclick={() => player.playTrack(contextMenuTarget!, fetch)}>
|
||||||
<form
|
<Music2 class="mr-1 size-4" />
|
||||||
class="flex w-full"
|
Play
|
||||||
method="POST"
|
|
||||||
action="/tracks/{contextMenuTarget!}?/play"
|
|
||||||
use:enhance={submitPlayTrack}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="flex w-full items-center px-2 py-1.5 focus:outline-0"
|
|
||||||
onclick={contextMenuItemClicked}
|
|
||||||
>
|
|
||||||
<Music2 class="mr-1 size-4" />
|
|
||||||
Play
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
|
|
||||||
<ContextMenu.Separator />
|
<ContextMenu.Separator />
|
||||||
@@ -187,19 +140,11 @@
|
|||||||
<ContextMenu.SubContent>
|
<ContextMenu.SubContent>
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<form {...props} method="POST" use:enhance={addTrackToPlaylist}>
|
<form {...props} method="POST" use:enhance={addTrackToPlaylist}>
|
||||||
{#each library.playlists as playlist}
|
{#each library.playlists as [_, playlist]}
|
||||||
<ContextMenu.Item>
|
<ContextMenu.Item
|
||||||
{#snippet child({ props })}
|
onclick={() => library.addTrackToPlaylist(playlist.id, contextMenuTarget!, fetch)}
|
||||||
<button
|
>
|
||||||
{...props}
|
{playlist.name}
|
||||||
type="submit"
|
|
||||||
onclick={contextMenuItemClicked}
|
|
||||||
formaction="/playlists/{playlist.id}/add-track/{contextMenuTarget!}"
|
|
||||||
class={cn(props.class ?? '', 'w-full')}
|
|
||||||
>
|
|
||||||
{playlist.name}
|
|
||||||
</button>
|
|
||||||
{/snippet}
|
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
{/each}
|
{/each}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user