diff --git a/protos/library.proto b/protos/library.proto index 4be63f6..353cec5 100644 --- a/protos/library.proto +++ b/protos/library.proto @@ -11,6 +11,7 @@ service Library { rpc DeletePlaylist(DeletePlaylistRequest) returns (google.protobuf.Empty); rpc AddTrackToPlaylist(AddTrackToPlaylistRequest) returns (google.protobuf.Empty); rpc RemoveTrackFromPlaylist(RemoveTrackFromPlaylistRequest) returns (google.protobuf.Empty); + rpc SwapTracks(SwapTracksRequest) returns (TrackList); } message TrackList { @@ -56,3 +57,9 @@ message RemoveTrackFromPlaylistRequest { uint32 playlist_id = 1; uint32 track_rank = 2; } + +message SwapTracksRequest { + uint32 playlist_id = 1; + uint32 a_rank = 2; + uint32 b_rank = 3; +} diff --git a/src/lib/proto/library.client.ts b/src/lib/proto/library.client.ts index 614bca7..9483025 100644 --- a/src/lib/proto/library.client.ts +++ b/src/lib/proto/library.client.ts @@ -4,6 +4,7 @@ import type { RpcTransport } from "@protobuf-ts/runtime-rpc"; import type { ServiceInfo } from "@protobuf-ts/runtime-rpc"; import { Library } from "./library"; +import type { SwapTracksRequest } from "./library"; import type { RemoveTrackFromPlaylistRequest } from "./library"; import type { AddTrackToPlaylistRequest } from "./library"; import type { DeletePlaylistRequest } from "./library"; @@ -43,6 +44,10 @@ export interface ILibraryClient { * @generated from protobuf rpc: RemoveTrackFromPlaylist(library.RemoveTrackFromPlaylistRequest) returns (google.protobuf.Empty); */ removeTrackFromPlaylist(input: RemoveTrackFromPlaylistRequest, options?: RpcOptions): UnaryCall; + /** + * @generated from protobuf rpc: SwapTracks(library.SwapTracksRequest) returns (library.TrackList); + */ + swapTracks(input: SwapTracksRequest, options?: RpcOptions): UnaryCall; } /** * @generated from protobuf service library.Library @@ -95,4 +100,11 @@ export class LibraryClient implements ILibraryClient, ServiceInfo { const method = this.methods[5], opt = this._transport.mergeOptions(options); return stackIntercept("unary", this._transport, method, opt, input); } + /** + * @generated from protobuf rpc: SwapTracks(library.SwapTracksRequest) returns (library.TrackList); + */ + swapTracks(input: SwapTracksRequest, options?: RpcOptions): UnaryCall { + const method = this.methods[6], opt = this._transport.mergeOptions(options); + return stackIntercept("unary", this._transport, method, opt, input); + } } diff --git a/src/lib/proto/library.ts b/src/lib/proto/library.ts index 490edc1..dff3b4f 100644 --- a/src/lib/proto/library.ts +++ b/src/lib/proto/library.ts @@ -125,6 +125,23 @@ export interface RemoveTrackFromPlaylistRequest { */ trackRank: number; } +/** + * @generated from protobuf message library.SwapTracksRequest + */ +export interface SwapTracksRequest { + /** + * @generated from protobuf field: uint32 playlist_id = 1; + */ + playlistId: number; + /** + * @generated from protobuf field: uint32 a_rank = 2; + */ + aRank: number; + /** + * @generated from protobuf field: uint32 b_rank = 3; + */ + bRank: number; +} // @generated message type with reflection information, may provide speed optimized methods class TrackList$Type extends MessageType { constructor() { @@ -611,6 +628,69 @@ class RemoveTrackFromPlaylistRequest$Type extends MessageType { + constructor() { + super("library.SwapTracksRequest", [ + { no: 1, name: "playlist_id", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 2, name: "a_rank", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }, + { no: 3, name: "b_rank", kind: "scalar", T: 13 /*ScalarType.UINT32*/ } + ]); + } + create(value?: PartialMessage): SwapTracksRequest { + const message = globalThis.Object.create((this.messagePrototype!)); + message.playlistId = 0; + message.aRank = 0; + message.bRank = 0; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SwapTracksRequest): SwapTracksRequest { + 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 a_rank */ 2: + message.aRank = reader.uint32(); + break; + case /* uint32 b_rank */ 3: + message.bRank = 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: SwapTracksRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* uint32 playlist_id = 1; */ + if (message.playlistId !== 0) + writer.tag(1, WireType.Varint).uint32(message.playlistId); + /* uint32 a_rank = 2; */ + if (message.aRank !== 0) + writer.tag(2, WireType.Varint).uint32(message.aRank); + /* uint32 b_rank = 3; */ + if (message.bRank !== 0) + writer.tag(3, WireType.Varint).uint32(message.bRank); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message library.SwapTracksRequest + */ +export const SwapTracksRequest = new SwapTracksRequest$Type(); /** * @generated ServiceType for protobuf service library.Library */ @@ -620,5 +700,6 @@ export const Library = new ServiceType("library.Library", [ { 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 } + { name: "RemoveTrackFromPlaylist", options: {}, I: RemoveTrackFromPlaylistRequest, O: Empty }, + { name: "SwapTracks", options: {}, I: SwapTracksRequest, O: TrackList } ]); diff --git a/src/routes/playlists/[id]/+page.server.ts b/src/routes/playlists/[id]/+page.server.ts index 589600c..c78e7e8 100644 --- a/src/routes/playlists/[id]/+page.server.ts +++ b/src/routes/playlists/[id]/+page.server.ts @@ -4,6 +4,8 @@ import { PlayerClient } from '$lib/proto/player.client'; import { LibraryClient } from '$lib/proto/library.client'; import { serializable } from '$lib/proto'; import type { PlayerStatus } from '$lib/proto/player'; +import { fail } from '@sveltejs/kit'; +import type { TrackList } from '$lib/proto/library'; export const actions = { play: async ({ params }) => { @@ -19,5 +21,26 @@ export const actions = { const client = new LibraryClient(protoTransport); const _response = await client.deletePlaylist({ id: parseInt(params.id) }); + }, + 'swap-tracks': async ({ params, request }) => { + const formData = await request.formData(); + + const a = formData.get('a')?.toString(); + const b = formData.get('b')?.toString(); + + if (!a || !b) { + return fail(400); + } + + const client = new LibraryClient(protoTransport); + + const response = await client.swapTracks({ + playlistId: parseInt(params.id), + aRank: parseInt(a), + bRank: parseInt(b) + }); + + console.log(response.response.tracks); + return serializable(response.response); } } satisfies Actions; diff --git a/src/routes/playlists/[id]/+page.svelte b/src/routes/playlists/[id]/+page.svelte index 103a84b..0c45d7e 100644 --- a/src/routes/playlists/[id]/+page.svelte +++ b/src/routes/playlists/[id]/+page.svelte @@ -7,9 +7,10 @@ import duration from 'dayjs/plugin/duration'; import { getCoverUrl } from '$lib/covers'; import Play from 'virtual:icons/lucide/play'; - import Shuffle from 'virtual:icons/lucide/shuffle'; import { getPlayerState } from '$lib/player.svelte'; import { Button } from '$lib/components/ui/button'; + import { deserialize } from '$app/forms'; + import type { Track, TrackList } from '$lib/proto/library'; dayjs.extend(duration); const library = getLibraryState(); @@ -20,6 +21,48 @@ let totalDuration = $derived( playlist?.tracks.reduce((acc, track) => acc + Number(track.duration), 0) ?? 0 ); + + function onDragStart(e: DragEvent) { + const element = e.target as HTMLElement; + + e.dataTransfer?.setData('text/plain', element.getAttribute('data-playlist-index')!); + } + + async function onDrop(e: DragEvent) { + const aIndex = e.dataTransfer?.getData('text/plain')!; + 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; + } + + const result = deserialize(await response.text()); + + if (result.type === 'success' && result.data && 'tracks' in result.data) { + 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; + } + } + } {#if playlist} @@ -40,7 +83,8 @@
- player.playPlaylist(playlist.id, fetch)} + >
@@ -54,9 +98,16 @@ - {#each playlist.tracks as track, i} - player.playTrack(track.hash, fetch)}> + player.playTrack(track.hash, fetch)} + data-playlist-index={i} + ondragstart={onDragStart} + ondrop={onDrop} + ondragover={(e) => e.preventDefault()} + draggable + > {i + 1}